Merge "Revert "Adding screen record for the bug where we can't find a folder after dragging"" into main
diff --git a/Android.bp b/Android.bp
index e358005..73d0fce 100644
--- a/Android.bp
+++ b/Android.bp
@@ -17,7 +17,7 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-min_launcher3_sdk_version = "30"
+min_launcher3_sdk_version = "31"
 
 // Targets that don't inherit framework aconfig libs (i.e., those that don't set
 // `platform_apis: true`) must manually link them.
@@ -27,7 +27,7 @@
         "android.os.flags-aconfig-java",
         "android.appwidget.flags-aconfig-java",
         "com.android.window.flags.window-aconfig-java",
-    ]
+    ],
 }
 
 // Common source files used to build launcher (java and kotlin)
@@ -42,12 +42,122 @@
     ],
 }
 
+// Main Launcher source for compose, excluding the build config
+filegroup {
+    name: "launcher-compose-enabled-src",
+    srcs: [
+        "compose/facade/enabled/*.kt",
+        "compose/facade/core/*.kt",
+        "compose/features/**/*.kt",
+    ],
+}
+
+filegroup {
+    name: "launcher-compose-disabled-src",
+    srcs: [
+        "compose/facade/core/*.kt",
+        "compose/facade/disabled/*.kt",
+    ],
+}
+
 // Source code for quickstep build, on top of launcher-src
 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
+filegroup {
+    name: "launcher-quickstep-dagger",
+    srcs: [
+        "quickstep/dagger/**/*.java",
+        "quickstep/dagger/**/*.kt",
+    ],
+}
+
+// Source code for quickstep build with compose enabled, on top of launcher-src
+filegroup {
+    name: "launcher-quickstep-compose-enabled-src",
+    srcs: [
+        "quickstep/compose/facade/core/*.kt",
+        "quickstep/compose/facade/enabled/*.kt",
+        "quickstep/compose/features/**/*.kt",
+    ],
+}
+
+filegroup {
+    name: "launcher-quickstep-compose-disabled-src",
+    srcs: [
+        "quickstep/compose/facade/core/*.kt",
+        "quickstep/compose/facade/disabled/*.kt",
     ],
 }
 
@@ -74,10 +184,118 @@
     srcs: ["proguard.flags"],
 }
 
+// Opt-in configuration for Launcher3 code depending on Jetpack Compose.
+soong_config_module_type {
+    name: "launcher_compose_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: ["release_enable_compose_in_launcher"],
+    properties: [
+        "srcs",
+        "static_libs",
+    ],
+}
+
+// Opt-in configuration for Launcher Quickstep code depending on Jetpack Compose.
+soong_config_bool_variable {
+    name: "release_enable_compose_in_launcher",
+}
+
+soong_config_module_type {
+    name: "quickstep_compose_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: ["release_enable_compose_in_launcher"],
+    properties: [
+        "srcs",
+        "static_libs",
+    ],
+}
+
+soong_config_module_type {
+    name: "launcher_compose_tests_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: ["release_enable_compose_in_launcher"],
+    properties: [
+        "static_libs",
+    ],
+}
+
+launcher_compose_java_defaults {
+    name: "launcher_compose_defaults",
+    soong_config_variables: {
+        release_enable_compose_in_launcher: {
+            srcs: [
+                ":launcher-compose-enabled-src",
+            ],
+
+            // Compose dependencies
+            static_libs: [
+                "androidx.compose.runtime_runtime",
+                "androidx.compose.material3_material3",
+            ],
+
+            // By default, Compose is disabled and we compile the ComposeFacade
+            // in compose/launcher3/facade/disabled/.
+            conditions_default: {
+                srcs: [
+                    ":launcher-compose-disabled-src",
+                ],
+                static_libs: [],
+            },
+        },
+    },
+}
+
+quickstep_compose_java_defaults {
+    name: "quickstep_compose_defaults",
+    soong_config_variables: {
+        release_enable_compose_in_launcher: {
+            srcs: [
+                ":launcher-quickstep-compose-enabled-src",
+            ],
+
+            // Compose dependencies
+            static_libs: [
+                "androidx.compose.runtime_runtime",
+                "androidx.compose.material3_material3",
+            ],
+
+            // By default, Compose is disabled and we compile the ComposeFacade
+            // in compose/quickstep/facade/disabled/.
+            conditions_default: {
+                srcs: [
+                    ":launcher-quickstep-compose-disabled-src",
+                ],
+                static_libs: [],
+            },
+        },
+    },
+}
+
+launcher_compose_tests_java_defaults {
+    name: "launcher_compose_tests_defaults",
+    soong_config_variables: {
+        release_enable_compose_in_launcher: {
+            // Compose dependencies
+            static_libs: [
+                "androidx.compose.runtime_runtime",
+                "androidx.compose.ui_ui-test-junit4",
+                "androidx.compose.ui_ui-test-manifest",
+            ],
+
+            conditions_default: {
+                static_libs: [],
+            },
+        },
+    },
+}
+
 android_library {
     name: "launcher-aosp-tapl",
     libs: [
-        "framework-statsd",
+        "framework-statsd.stubs.module_lib",
     ],
     static_libs: [
         "androidx.annotation_annotation",
@@ -87,6 +305,7 @@
         "androidx.preference_preference",
         "SystemUISharedLib",
         "//frameworks/libs/systemui:animationlib",
+        "//frameworks/libs/systemui:contextualeducationlib",
         "launcher-testing-shared",
     ],
     srcs: [
@@ -147,6 +366,9 @@
 // Library with all the dependencies for building Launcher3
 android_library {
     name: "Launcher3ResLib",
+    defaults: [
+        "launcher_compose_defaults",
+    ],
     srcs: [],
     resource_dirs: ["res"],
     static_libs: [
@@ -164,6 +386,8 @@
         "//frameworks/libs/systemui:iconloader_base",
         "//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",
@@ -173,6 +397,9 @@
         "kotlinx_coroutines",
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
+        "dagger2",
+        "jsr330",
+        "com_android_systemui_shared_flags_lib",
     ],
     manifest: "AndroidManifest-common.xml",
     sdk_version: "current",
@@ -207,6 +434,7 @@
     sdk_version: "current",
     min_sdk_version: min_launcher3_sdk_version,
     target_sdk_version: "current",
+    plugins: ["dagger2-compiler"],
     privileged: true,
     system_ext_specific: true,
 
@@ -235,13 +463,15 @@
         "quickstep/res",
     ],
     libs: [
-        "framework-statsd",
+        "framework-statsd.stubs.module_lib",
     ],
     static_libs: [
         "Launcher3ResLib",
         "lottie",
         "SystemUISharedLib",
         "SettingsLibSettingsTheme",
+        "dagger2",
+        "protolog-group",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     min_sdk_version: "current",
@@ -250,9 +480,14 @@
 // Library with all the source code and dependencies for building Launcher Go
 android_library {
     name: "Launcher3GoLib",
+    defaults: [
+        "launcher_compose_defaults",
+        "quickstep_compose_defaults",
+    ],
     srcs: [
         ":launcher-src",
         ":launcher-quickstep-src",
+        ":launcher-quickstep-dagger",
         "go/quickstep/src/**/*.java",
         "go/quickstep/src/**/*.kt",
     ],
@@ -267,7 +502,10 @@
         "QuickstepResLib",
         "androidx.room_room-runtime",
     ],
-    plugins: ["androidx.room_room-compiler-plugin"],
+    plugins: [
+        "androidx.room_room-compiler-plugin",
+        "dagger2-compiler",
+    ],
     manifest: "quickstep/AndroidManifest.xml",
     additional_manifests: [
         "go/AndroidManifest.xml",
@@ -281,14 +519,19 @@
 // Library with all the source code and dependencies for building Quickstep
 android_library {
     name: "Launcher3QuickStepLib",
+    defaults: [
+        "launcher_compose_defaults",
+        "quickstep_compose_defaults",
+    ],
     srcs: [
         ":launcher-src",
         ":launcher-quickstep-src",
+        ":launcher-quickstep-dagger",
         ":launcher-build-config",
     ],
     resource_dirs: [],
     libs: [
-        "framework-statsd",
+        "framework-statsd.stubs.module_lib",
     ],
     // Note the ordering here is important when it comes to resource
     // overriding. We want the most specific resource overrides defined
@@ -300,6 +543,7 @@
     ],
     manifest: "quickstep/AndroidManifest.xml",
     platform_apis: true,
+    plugins: ["dagger2-compiler"],
     min_sdk_version: "current",
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
@@ -308,7 +552,6 @@
 // Build rule for Quickstep app.
 android_app {
     name: "Launcher3QuickStep",
-
     static_libs: ["Launcher3QuickStepLib"],
     optimize: {
         proguard_flags_files: [":launcher-proguard-rules"],
@@ -327,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"],
 
@@ -343,13 +589,11 @@
 
 }
 
-
 // Build rule for Launcher3 Go app with quickstep for Android Go devices.
 // Note that the following two rules are exactly same, and should
 // eventually be merged into a single target
 android_app {
     name: "Launcher3Go",
-
     static_libs: ["Launcher3GoLib"],
     resource_dirs: [],
 
@@ -384,9 +628,9 @@
         include_filter: ["com.android.launcher3.*"],
     },
 }
+
 android_app {
     name: "Launcher3QuickStepGo",
-
     static_libs: ["Launcher3GoLib"],
     resource_dirs: [],
 
diff --git a/aconfig/Android.bp b/aconfig/Android.bp
index bca7494..5413601 100644
--- a/aconfig/Android.bp
+++ b/aconfig/Android.bp
@@ -20,7 +20,7 @@
 aconfig_declarations {
     name: "com_android_launcher3_flags",
     package: "com.android.launcher3",
-    container: "system_ext",
+    container: "system",
     srcs: ["**/*.aconfig"],
 }
 
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 21b9863..878aa6e 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.launcher3"
-container: "system_ext"
+container: "system"
 
 flag {
     name: "enable_expanding_pause_work_button"
@@ -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"
@@ -311,8 +325,205 @@
 }
 
 flag {
-    name: "enable_new_archiving_icon"
+    name: "enable_multi_instance_menu_taskbar"
     namespace: "launcher"
-    description: "Archived apps will use new icon in app title"
-    bug: "350758155"
+    description: "Menu in Taskbar with options to launch and manage multiple instances of the same app"
+    bug: "355237285"
 }
+
+flag {
+    name: "navigate_to_child_preference"
+    namespace: "launcher"
+    description: "Settings screen supports navigating to child preference if the key is not on the screen"
+    bug: "293390881"
+}
+
+flag {
+    name: "use_new_icon_for_archived_apps"
+    namespace: "launcher"
+    description: "Archived apps will use new cloud icon in app title instead of overlay"
+    bug: "350758155"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "letter_fast_scroller"
+    namespace: "launcher"
+    description: "Change fast scroller to a lettered list"
+    bug: "358673724"
+}
+
+flag {
+    name: "enable_desktop_task_alpha_animation"
+    namespace: "launcher"
+    description: "Enables the animation of the desktop task's background view"
+    bug: "320307666"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "ignore_three_finger_trackpad_for_nav_handle_long_press"
+    namespace: "launcher"
+    description: "Ignore three finger trackpad event for nav handle long press"
+    bug: "342143522"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+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"
+    bug: "364711064"
+}
+
+flag {
+    name: "one_grid_mounted_mode"
+    namespace: "launcher"
+    description: "Support a fixed landscape mode for handheld devices"
+    bug: "364711735"
+}
+
+flag {
+    name: "one_grid_rotation_handling"
+    namespace: "launcher"
+    description: "New landscape approach for the workspace using different rows and columns in landscape and portrait"
+    bug: "364711814"
+}
+
+flag {
+    name: "grid_migration_refactor"
+    namespace: "launcher"
+    description: "Refactor grid migration such that the code is simpler to understand and update"
+    bug: "358399271"
+}
+
+flag {
+    name: "accessibility_scroll_on_allapps"
+    namespace: "launcher"
+    description: "Scroll to item position if accessibility focused"
+    bug: "265392261"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "enable_dismiss_prediction_undo"
+    namespace: "launcher"
+    description: "Show an 'Undo' snackbar when users dismiss a predicted hotseat item"
+    bug: "270394476"
+}
+
+flag {
+    name: "enable_all_apps_button_in_hotseat"
+    namespace: "launcher"
+    description: "Enables displaying the all apps button in the hotseat."
+    bug: "270393897"
+}
+
+flag {
+    name: "taskbar_quiet_mode_change_support"
+    namespace: "launcher"
+    description: "Support changing quiet mode for user profiles in taskbar."
+    bug: "345760034"
+}
+
+flag {
+    name: "taskbar_overflow"
+    namespace: "launcher"
+    description: "Show recent apps in the taskbar overflow."
+    bug: "368119679"
+}
+
+flag {
+    name: "enable_active_gesture_proto_log"
+    namespace: "launcher"
+    description: "Enables tracking active gesture logs in ProtoLog"
+    bug: "293182501"
+}
+
+flag {
+    name: "enable_recents_window_proto_log"
+    namespace: "launcher"
+    description: "Enables tracking recents window logs in ProtoLog"
+    bug: "292269949"
+}
+
+flag {
+    name: "enable_state_manager_proto_log"
+    namespace: "launcher"
+    description: "Enables tracking state manager logs in ProtoLog"
+    bug: "292269949"
+}
+
+flag {
+    name: "coordinate_workspace_scale"
+    namespace: "launcher"
+    description: "Ensure that the workspace and hotseat scale doesn't conflict and transitions smoothly between launching and closing apps"
+    bug: "366403487"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "enable_tiered_widgets_by_default_in_picker"
+    namespace: "launcher"
+    description: "Shows filtered set of widgets by default and an option to show all widgets in the widget picker"
+    bug: "356127021"
+}
+
+flag {
+    name: "show_taskbar_pinning_popup_from_anywhere"
+    namespace: "launcher"
+    description: "Shows the pinning popup view after long-pressing or right-clicking anywhere on the pinned taskbar"
+    bug: "297325541"
+}
+
+flag {
+    name: "enable_launcher_overview_in_window"
+    namespace: "launcher"
+    description: "Enables launcher recents opening inside of a window instead of being hosted in launcher activity."
+    bug: "292269949"
+}
+
+flag {
+   name: "enforce_system_radius_for_app_widgets"
+   namespace: "launcher"
+   description: "Enforce system radius for widget corners instead of a separate 16.dp value"
+   bug: "370950552"
+}
+
+flag {
+    name: "enable_contrast_tiles"
+    namespace: "launcher"
+    description: "Enable launcher app contrast tiles."
+    bug: "341217082"
+}
+
+flag {
+  name: "msdl_feedback"
+  namespace: "launcher"
+  description: "Enable MSDL feedback for Launcher interactions"
+  bug: "377496684"
+}
+
+flag {
+    name: "taskbar_recents_layout_transition"
+    namespace: "launcher"
+    description: "Enable Taskbar LayoutTransition for Recent Apps"
+    bug: "343521765"
+}
\ No newline at end of file
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index f9327fe..93d8d54 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.launcher3"
-container: "system_ext"
+container: "system"
 
 flag {
     name: "enable_grid_only_overview"
@@ -21,3 +21,44 @@
     description: "Enables rewritten version of TaskThumbnailViews in Overview"
     bug: "331753115"
 }
+
+flag {
+    name: "enable_hover_of_child_elements_in_taskview"
+    namespace: "launcher_overview"
+    description: "Enables child elements to receive hover events in TaskView and respond visually to those hover events."
+    bug: "342594235"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    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: "357860832"
+}
+
+flag {
+    name: "enable_overview_command_helper_timeout"
+    namespace: "launcher_overview"
+    description: "Enables OverviewCommandHelper new version with a timeout to prevent the queue to be unresponsive."
+    bug: "351122926"
+    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/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index b98eee6..72f654e 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.launcher3"
-container: "system_ext"
+container: "system"
 
 flag {
     name: "enable_private_space"
diff --git a/compose/facade/core/BaseComposeFacade.kt b/compose/facade/core/BaseComposeFacade.kt
new file mode 100644
index 0000000..bc7ba47
--- /dev/null
+++ b/compose/facade/core/BaseComposeFacade.kt
@@ -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.
+ */
+
+package com.android.launcher3.compose.core
+
+import android.content.Context
+import android.view.View
+
+interface BaseComposeFacade {
+    fun isComposeAvailable(): Boolean
+
+    fun initComposeView(appContext: Context): View
+}
diff --git a/compose/facade/disabled/ComposeFacade.kt b/compose/facade/disabled/ComposeFacade.kt
new file mode 100644
index 0000000..c1cbfff
--- /dev/null
+++ b/compose/facade/disabled/ComposeFacade.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.compose
+
+import android.content.Context
+import android.view.View
+import com.android.launcher3.compose.core.BaseComposeFacade
+
+object ComposeFacade : BaseComposeFacade {
+    override fun isComposeAvailable(): Boolean = false
+
+    override fun initComposeView(appContext: Context): View {
+        error(
+            "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
+                " other function on ComposeFacade."
+        )
+    }
+}
diff --git a/compose/facade/enabled/ComposeFacade.kt b/compose/facade/enabled/ComposeFacade.kt
new file mode 100644
index 0000000..d98a979f
--- /dev/null
+++ b/compose/facade/enabled/ComposeFacade.kt
@@ -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.
+ */
+
+package com.android.launcher3.compose
+
+import android.content.Context
+import android.view.View
+import androidx.compose.ui.platform.ComposeView
+import com.android.launcher3.compose.core.BaseComposeFacade
+
+object ComposeFacade : BaseComposeFacade {
+    override fun isComposeAvailable(): Boolean = true
+
+    override fun initComposeView(appContext: Context): View = ComposeView(appContext)
+}
diff --git a/go/quickstep/res/values-be/strings.xml b/go/quickstep/res/values-be/strings.xml
index 83374bb..de8766f 100644
--- a/go/quickstep/res/values-be/strings.xml
+++ b/go/quickstep/res/values-be/strings.xml
@@ -4,7 +4,7 @@
     <string name="app_share_drop_target_label" msgid="5804774105974539508">"Абагуліць праграму"</string>
     <string name="action_listen" msgid="2370304050784689486">"Праслухаць"</string>
     <string name="action_translate" msgid="8028378961867277746">"Перакласці"</string>
-    <string name="action_search" msgid="6269564710943755464">"Аб\'ектыў"</string>
+    <string name="action_search" msgid="6269564710943755464">"Аб’ектыў"</string>
     <string name="dialog_acknowledge" msgid="2804025517675853172">"ЗРАЗУМЕЛА"</string>
     <string name="dialog_cancel" msgid="6464336969134856366">"СКАСАВАЦЬ"</string>
     <string name="dialog_settings" msgid="6564397136021186148">"НАЛАДЫ"</string>
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/res/values/styles.xml b/go/quickstep/res/values/styles.xml
index c659331..2524c76 100644
--- a/go/quickstep/res/values/styles.xml
+++ b/go/quickstep/res/values/styles.xml
@@ -16,7 +16,7 @@
 -->
 <resources>
     <!-- App themes -->
-    <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
+    <style name="LauncherTheme" parent="@style/DynamicColorsBaseLauncherTheme">
         <item name="overviewButtonTextColor">@color/go_overview_text_color</item>
         <item name="overviewButtonIconColor">@color/go_overview_text_color</item>
         <item name="overviewButtonBackgroundColor">@color/go_overview_button_color</item>
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/Android.bp b/quickstep/Android.bp
index f14cebd..2ef9f82 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -52,6 +52,23 @@
         "tests/src/com/android/quickstep/TaplOverviewIconTest.java",
         "tests/src/com/android/quickstep/TaplTestsQuickstep.java",
         "tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
-        "tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java"
+        "tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt",
+        "tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java",
+    ],
+}
+
+filegroup {
+    name: "launcher3-quickstep-screenshot-tests-src",
+    path: "tests/multivalentScreenshotTests",
+    srcs: [
+        "tests/multivalentScreenshotTests/src/**/*.kt",
+    ],
+}
+
+filegroup {
+    name: "launcher3-quickstep-testing",
+    path: "testing",
+    srcs: [
+        "testing/**/*.kt",
     ],
 }
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 80d8154..c6e2d8c 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -48,7 +48,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 4abf6e1..57bfb4a 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -54,6 +54,7 @@
         android:protectionLevel="signature|privileged" />
 
     <application android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+         android:enableOnBackInvokedCallback="true"
          android:fullBackupOnly="true"
          android:fullBackupContent="@xml/backupscheme"
          android:hardwareAccelerated="true"
@@ -80,7 +81,7 @@
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
              android:screenOrientation="behind"
-             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
              android:enableOnBackInvokedCallback="false"
diff --git a/quickstep/compose/facade/core/QuickstepComposeFeatures.kt b/quickstep/compose/facade/core/QuickstepComposeFeatures.kt
new file mode 100644
index 0000000..ca9e5c9
--- /dev/null
+++ b/quickstep/compose/facade/core/QuickstepComposeFeatures.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.compose.core
+
+interface QuickstepComposeFeatures
diff --git a/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt b/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt
new file mode 100644
index 0000000..0a4345a
--- /dev/null
+++ b/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.compose
+
+import android.content.Context
+import com.android.launcher3.compose.ComposeFacade
+import com.android.launcher3.compose.core.BaseComposeFacade
+import com.android.quickstep.compose.core.QuickstepComposeFeatures
+
+object QuickstepComposeFacade : BaseComposeFacade, QuickstepComposeFeatures {
+
+    override fun isComposeAvailable() = ComposeFacade.isComposeAvailable()
+
+    override fun initComposeView(appContext: Context) = ComposeFacade.initComposeView(appContext)
+}
diff --git a/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt b/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt
new file mode 100644
index 0000000..97cd300
--- /dev/null
+++ b/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt
@@ -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.
+ */
+
+package com.android.quickstep.compose
+
+import android.content.Context
+import com.android.launcher3.compose.ComposeFacade
+import com.android.launcher3.compose.core.BaseComposeFacade
+import com.android.quickstep.compose.core.QuickstepComposeFeatures
+
+object QuickstepComposeFacade : BaseComposeFacade, QuickstepComposeFeatures {
+    override fun isComposeAvailable() = ComposeFacade.isComposeAvailable()
+
+    override fun initComposeView(appContext: Context) = ComposeFacade.initComposeView(appContext)
+}
diff --git a/quickstep/dagger/LauncherAppComponent.java b/quickstep/dagger/LauncherAppComponent.java
new file mode 100644
index 0000000..068f01c
--- /dev/null
+++ b/quickstep/dagger/LauncherAppComponent.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.android.quickstep.dagger.QuickStepModule;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
+
+import dagger.Component;
+
+/**
+ * Root component for Dagger injection for Launcher Quickstep.
+ */
+@LauncherAppSingleton
+@Component(modules = QuickStepModule.class)
+public interface LauncherAppComponent extends QuickstepBaseAppComponent {
+    /** Builder for quickstep LauncherAppComponent. */
+    @Component.Builder
+    interface Builder extends LauncherBaseAppComponent.Builder {
+        LauncherAppComponent build();
+    }
+}
diff --git a/quickstep/res/color/all_set_bg_primary.xml b/quickstep/res/color/all_set_bg_primary.xml
index 4cf857d..013de7a 100644
--- a/quickstep/res/color/all_set_bg_primary.xml
+++ b/quickstep/res/color/all_set_bg_primary.xml
@@ -15,5 +15,5 @@
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorPrimaryContainer"/>
+    <item android:color="?attr/materialColorPrimaryContainer"/>
 </selector>
diff --git a/quickstep/res/color/all_set_bg_tertiary.xml b/quickstep/res/color/all_set_bg_tertiary.xml
index e62b094..b58d61c 100644
--- a/quickstep/res/color/all_set_bg_tertiary.xml
+++ b/quickstep/res/color/all_set_bg_tertiary.xml
@@ -15,5 +15,5 @@
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorTertiary"/>
+    <item android:color="?attr/materialColorTertiary"/>
 </selector>
diff --git a/quickstep/res/color/bubblebar_drop_target_bg_color.xml b/quickstep/res/color/bubblebar_drop_target_bg_color.xml
index ca37c7f..bae8c4e 100644
--- a/quickstep/res/color/bubblebar_drop_target_bg_color.xml
+++ b/quickstep/res/color/bubblebar_drop_target_bg_color.xml
@@ -15,5 +15,5 @@
   -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" />
+    <item android:alpha="0.35" android:color="?attr/materialColorPrimaryContainer" />
 </selector>
\ No newline at end of file
diff --git a/quickstep/res/color/menu_item_hover_state_color.xml b/quickstep/res/color/menu_item_hover_state_color.xml
index 1dd7654..3c68789 100644
--- a/quickstep/res/color/menu_item_hover_state_color.xml
+++ b/quickstep/res/color/menu_item_hover_state_color.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:state_hovered="false" android:color="?androidprv:attr/materialColorSurfaceBright" />
-    <item android:state_hovered="true" android:color="?androidprv:attr/materialColorSurfaceVariant" />
+    <item android:state_hovered="false" android:color="?attr/materialColorSurfaceBright" />
+    <item android:state_hovered="true" android:color="?attr/materialColorSurfaceVariant" />
 </selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface.xml b/quickstep/res/color/taskbar_minimized_app_indicator_color.xml
similarity index 82%
rename from res/color-night-v31/material_color_surface.xml
rename to quickstep/res/color/taskbar_minimized_app_indicator_color.xml
index a645f24..1596fe1 100644
--- a/res/color-night-v31/material_color_surface.xml
+++ b/quickstep/res/color/taskbar_minimized_app_indicator_color.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ 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.
@@ -15,5 +15,5 @@
   ~ limitations under the License.
   -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
+    <item android:color="?attr/materialColorOutline"/>
+</selector>
diff --git a/res/color-night-v31/material_color_surface.xml b/quickstep/res/color/taskbar_running_app_indicator_color.xml
similarity index 82%
copy from res/color-night-v31/material_color_surface.xml
copy to quickstep/res/color/taskbar_running_app_indicator_color.xml
index a645f24..5dc9781 100644
--- a/res/color-night-v31/material_color_surface.xml
+++ b/quickstep/res/color/taskbar_running_app_indicator_color.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ 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.
@@ -15,5 +15,5 @@
   ~ limitations under the License.
   -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
+    <item android:color="?attr/materialColorTertiary"/>
+</selector>
diff --git a/quickstep/res/drawable/bg_bubble_bar_drop_target.xml b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
index 79e4318..f597cb5 100644
--- a/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
+++ b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
@@ -20,5 +20,5 @@
     <solid android:color="@color/bubblebar_drop_target_bg_color" />
     <stroke
         android:width="1dp"
-        android:color="?androidprv:attr/materialColorPrimaryContainer" />
+        android:color="?attr/materialColorPrimaryContainer" />
 </shape>
diff --git a/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
index d722dd7..169e396 100644
--- a/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
+++ b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
@@ -22,6 +22,6 @@
         <solid android:color="@color/bubblebar_drop_target_bg_color" />
         <stroke
             android:width="1dp"
-            android:color="?androidprv:attr/materialColorPrimaryContainer" />
+            android:color="?attr/materialColorPrimaryContainer" />
     </shape>
 </inset>
diff --git a/quickstep/res/drawable/bg_floating_desktop_select.xml b/quickstep/res/drawable/bg_floating_desktop_select.xml
index d7df338..6481be4 100644
--- a/quickstep/res/drawable/bg_floating_desktop_select.xml
+++ b/quickstep/res/drawable/bg_floating_desktop_select.xml
@@ -19,5 +19,5 @@
     android:shape="rectangle">
 
     <corners android:radius="@dimen/rounded_button_radius" />
-    <solid android:color="?androidprv:attr/materialColorPrimaryContainer" />
+    <solid android:color="?attr/materialColorPrimaryContainer" />
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_overview_clear_all_button.xml b/quickstep/res/drawable/bg_overview_clear_all_button.xml
index 143761f..0d12274 100644
--- a/quickstep/res/drawable/bg_overview_clear_all_button.xml
+++ b/quickstep/res/drawable/bg_overview_clear_all_button.xml
@@ -21,7 +21,7 @@
         <shape android:shape="rectangle"
             android:tint="?colorButtonNormal">
             <corners android:radius="@dimen/recents_clear_all_outline_radius" />
-            <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
+            <solid android:color="?attr/materialColorSurfaceBright"/>
         </shape>
     </item>
 </ripple>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
index 20c524d..9e9bb2b 100644
--- a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
+++ b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
@@ -18,5 +18,5 @@
     android:shape="rectangle">
 
     <corners android:radius="@dimen/dialogCornerRadius" />
-    <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
+    <solid android:color="?attr/materialColorSurfaceBright"/>
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_wellbeing_toast.xml b/quickstep/res/drawable/bg_wellbeing_toast.xml
index 0b66849..418caae 100644
--- a/quickstep/res/drawable/bg_wellbeing_toast.xml
+++ b/quickstep/res/drawable/bg_wellbeing_toast.xml
@@ -16,6 +16,6 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="?androidprv:attr/materialColorSecondaryFixed" />
+    <solid android:color="?attr/materialColorSecondaryFixed" />
     <corners android:radius="?android:attr/dialogCornerRadius" />
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_chevron_down.xml b/quickstep/res/drawable/ic_chevron_down.xml
index f246cbc..b586e50 100644
--- a/quickstep/res/drawable/ic_chevron_down.xml
+++ b/quickstep/res/drawable/ic_chevron_down.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:height="48dp"
     android:autoMirrored="true"
-    android:tint="?androidprv:attr/materialColorOnSurface"
+    android:tint="?attr/materialColorOnSurface"
     android:viewportHeight="48"
     android:viewportWidth="48">
     <group
diff --git a/quickstep/res/drawable/ic_desktop.xml b/quickstep/res/drawable/ic_desktop.xml
index 8de275d..11feca5 100644
--- a/quickstep/res/drawable/ic_desktop.xml
+++ b/quickstep/res/drawable/ic_desktop.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?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");
@@ -15,17 +14,13 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="32.0dp"
-    android:height="32.0dp"
-    android:viewportWidth="32.0"
-    android:viewportHeight="32.0"
-    >
-    <group android:scaleX="0.5"
-        android:scaleY="0.5"
-        android:translateX="6.0"
-        android:translateY="6.0">
-        <path
-            android:fillColor="@android:color/black"
-            android:pathData="M5.958,37.708Q4.458,37.708 3.354,36.604Q2.25,35.5 2.25,34V18.292Q2.25,16.792 3.354,15.688Q4.458,14.583 5.958,14.583H9.5V5.958Q9.5,4.458 10.625,3.354Q11.75,2.25 13.208,2.25H34Q35.542,2.25 36.646,3.354Q37.75,4.458 37.75,5.958V21.667Q37.75,23.167 36.646,24.271Q35.542,25.375 34,25.375H30.5V34Q30.5,35.5 29.396,36.604Q28.292,37.708 26.792,37.708ZM5.958,34H26.792Q26.792,34 26.792,34Q26.792,34 26.792,34V21.542H5.958V34Q5.958,34 5.958,34Q5.958,34 5.958,34ZM30.5,21.667H34Q34,21.667 34,21.667Q34,21.667 34,21.667V9.208H13.208V14.583H26.833Q28.375,14.583 29.438,15.667Q30.5,16.75 30.5,18.25Z"/>
-    </group>
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M240,640L600,640L600,440L240,440L240,640ZM660,520L720,520L720,320L360,320L360,380L660,380L660,520ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720Z" />
 </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/drawable/keyboard_quick_switch_overview_button_background.xml b/quickstep/res/drawable/keyboard_quick_switch_text_button_background.xml
similarity index 77%
rename from quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
rename to quickstep/res/drawable/keyboard_quick_switch_text_button_background.xml
index 2a4f087..f204920 100644
--- a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
+++ b/quickstep/res/drawable/keyboard_quick_switch_text_button_background.xml
@@ -15,8 +15,7 @@
 -->
 <shape
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
-    <corners android:radius="@dimen/keyboard_quick_switch_task_view_radius" />
+    <solid android:color="@android:color/white" />
+    <corners android:radius="@dimen/keyboard_quick_switch_text_button_radius" />
 </shape>
diff --git a/quickstep/res/drawable/rotate_tutorial_warning.xml b/quickstep/res/drawable/rotate_tutorial_warning.xml
index d1117a4..90b7d64 100644
--- a/quickstep/res/drawable/rotate_tutorial_warning.xml
+++ b/quickstep/res/drawable/rotate_tutorial_warning.xml
@@ -21,6 +21,6 @@
     android:viewportHeight="960"
     android:viewportWidth="960">
     <path
-        android:fillColor="?androidprv:attr/materialColorOnSurface"
+        android:fillColor="?attr/materialColorOnSurface"
         android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500L480,500Z" />
 </vector>
diff --git a/quickstep/res/drawable/taskbar_overflow_icon.xml b/quickstep/res/drawable/taskbar_overflow_icon.xml
new file mode 100644
index 0000000..b93a70e
--- /dev/null
+++ b/quickstep/res/drawable/taskbar_overflow_icon.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="48dp"
+    android:height="48dp"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@color/taskbar_divider_background"
+        android:pathData="M80,605v-250q0,-28.88 20.59,-49.44t49.5,-20.56q28.91,0 49.41,20.56Q220,326.12 220,355v250q0,28.87 -20.59,49.44Q178.82,675 149.91,675t-49.41,-20.56Q80,633.87 80,605ZM340,760q-24,0 -42,-18t-18,-42v-440q0,-24 18,-42t42,-18h280q24,0 42,18t18,42v440q0,24 -18,42t-42,18L340,760ZM740,605v-250q0,-28.88 20.59,-49.44t49.5,-20.56q28.91,0 49.41,20.56Q880,326.12 880,355v250q0,28.87 -20.59,49.44Q838.82,675 809.91,675t-49.41,-20.56Q740,633.87 740,605ZM340,700h280v-440L340,260v440ZM480,480Z"/>
+</vector>
diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
index 38df756..d1e5667 100644
--- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
@@ -18,17 +18,17 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
-    android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:importantForAccessibility="yes"
     android:background="@drawable/keyboard_quick_switch_task_view_background"
     android:clipToOutline="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
+        android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
 
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -52,7 +52,7 @@
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:visibility="gone"
-            android:layout_marginStart="@dimen/keyboard_quick_switch_split_view_spacing"
+            android:layout_marginStart="@dimen/keyboard_quick_switch_view_small_spacing"
 
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview_square.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview_square.xml
new file mode 100644
index 0000000..0eccd8e
--- /dev/null
+++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview_square.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:importantForAccessibility="yes"
+    android:background="@drawable/keyboard_quick_switch_task_view_background"
+    android:clipToOutline="true"
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/content"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+
+        app:layout_constraintDimensionRatio="1:1"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent">
+
+        <include
+            layout="@layout/keyboard_quick_switch_taskview_thumbnail"
+            android:id="@+id/thumbnail_1"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/thumbnail_2"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+        <include
+            layout="@layout/keyboard_quick_switch_taskview_thumbnail"
+            android:id="@+id/thumbnail_2"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:visibility="gone"
+            android:layout_marginTop="@dimen/keyboard_quick_switch_view_small_spacing"
+
+            app:layout_constraintTop_toBottomOf="@id/thumbnail_1"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+        <ImageView
+            android:id="@+id/icon_1"
+            android:layout_width="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:layout_height="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:importantForAccessibility="no"
+            android:scaleType="centerCrop"
+
+            app:layout_constraintTop_toTopOf="@id/thumbnail_1"
+            app:layout_constraintBottom_toBottomOf="@id/thumbnail_1"
+            app:layout_constraintStart_toStartOf="@id/thumbnail_1"
+            app:layout_constraintEnd_toEndOf="@id/thumbnail_1"/>
+
+        <ImageView
+            android:id="@+id/icon_2"
+            android:layout_width="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:layout_height="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:importantForAccessibility="no"
+            android:visibility="gone"
+            android:scaleType="centerCrop"
+
+            app:layout_constraintTop_toTopOf="@id/thumbnail_2"
+            app:layout_constraintBottom_toBottomOf="@id/thumbnail_2"
+            app:layout_constraintStart_toStartOf="@id/thumbnail_2"
+            app:layout_constraintEnd_toEndOf="@id/thumbnail_2"/>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView>
diff --git a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
index e4942ae..40d2322 100644
--- a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
@@ -20,7 +20,7 @@
     android:theme="@style/GestureTutorialActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="?androidprv:attr/materialColorSurfaceContainer"
+    android:background="?attr/materialColorSurfaceContainer"
     android:fitsSystemWindows="true">
 
     <androidx.constraintlayout.widget.ConstraintLayout
@@ -163,7 +163,7 @@
             android:layout_marginVertical="16dp"
             android:text="@string/gesture_tutorial_action_button_label"
             android:background="@drawable/gesture_tutorial_action_button_background"
-            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:backgroundTint="?attr/materialColorPrimary"
             android:stateListAnimator="@null"
 
             app:layout_constraintTop_toBottomOf="@id/guideline"
diff --git a/quickstep/res/layout/activity_allset.xml b/quickstep/res/layout/activity_allset.xml
index 685a151..625d9b3 100644
--- a/quickstep/res/layout/activity_allset.xml
+++ b/quickstep/res/layout/activity_allset.xml
@@ -96,7 +96,6 @@
                 style="@style/TextAppearance.GestureTutorial.Feedback.Subtitle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="@string/allset_hint"
                 android:textSize="@dimen/allset_page_swipe_up_text_size"
                 android:gravity="center_horizontal"
 
diff --git a/quickstep/res/layout/bubblebar_flyout.xml b/quickstep/res/layout/bubblebar_flyout.xml
new file mode 100644
index 0000000..e3338bf
--- /dev/null
+++ b/quickstep/res/layout/bubblebar_flyout.xml
@@ -0,0 +1,54 @@
+<?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.
+  -->
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <ImageView
+        android:id="@+id/bubble_flyout_icon"
+        android:layout_width="50dp"
+        android:layout_height="36dp"
+        android:paddingEnd="@dimen/bubblebar_flyout_avatar_message_space"
+        android:scaleType="centerInside"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:src="#ff0000"/>
+
+    <TextView
+        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_icon"
+        tools:text="Sender"/>
+
+    <TextView
+        android:id="@+id/bubble_flyout_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:fontFamily="@*android:string/config_bodyFontFamily"
+        android:maxLines="2"
+        android:ellipsize="end"
+        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
new file mode 100644
index 0000000..d988cbc
--- /dev/null
+++ b/quickstep/res/layout/customizable_taskbar.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.taskbar.TaskbarDragLayer
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/taskbar_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:clipChildren="false">
+
+    <!-- TODO(b/349885828) : to be removed in future cl. -->
+    <com.android.launcher3.taskbar.TaskbarView
+        android:id="@+id/taskbar_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:focusable="true"
+        android:importantForAccessibility="yes"
+        android:forceHasOverlappingRendering="false"
+        android:layout_gravity="bottom"
+        android:layout_marginBottom="@dimen/transient_taskbar_bottom_margin"
+        android:clipChildren="false" />
+
+    <com.android.launcher3.taskbar.customization.CustomizableTaskbarView
+        android:id="@+id/customizable_taskbar_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:focusable="true"
+        android:importantForAccessibility="yes"
+        android:forceHasOverlappingRendering="false"
+        android:layout_gravity="bottom"
+        android:layout_marginBottom="@dimen/transient_taskbar_bottom_margin"
+        android:clipChildren="false" />
+
+    <com.android.launcher3.taskbar.TaskbarScrimView
+        android:id="@+id/taskbar_scrim"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <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"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom" >
+
+        <FrameLayout
+            android:id="@+id/start_contextual_buttons"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:paddingStart="@dimen/taskbar_contextual_button_padding"
+            android:paddingEnd="@dimen/taskbar_contextual_button_padding"
+            android:paddingTop="@dimen/taskbar_contextual_padding_top"
+            android:gravity="center_vertical"
+            android:layout_gravity="start"/>
+
+        <LinearLayout
+            android:id="@+id/end_nav_buttons"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:orientation="horizontal"
+            android:gravity="center_vertical"
+            android:layout_gravity="end"/>
+
+        <FrameLayout
+            android:id="@+id/end_contextual_buttons"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:paddingTop="@dimen/taskbar_contextual_padding_top"
+            android:gravity="center_vertical"
+            android:layout_gravity="end"/>
+    </com.android.launcher3.taskbar.navbutton.NearestTouchFrame>
+
+    <com.android.launcher3.taskbar.StashedHandleView
+        android:id="@+id/stashed_handle"
+        tools:comment1="The actual size and shape will be set as a ViewOutlineProvider at runtime"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@color/taskbar_stashed_handle_dark_color"
+        android:clipToOutline="true"
+        android:layout_gravity="bottom"/>
+
+    <com.android.launcher3.taskbar.StashedHandleView
+        android:id="@+id/stashed_bubble_handle"
+        tools:comment1="The actual size and shape will be set as a ViewOutlineProvider at runtime"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:background="@color/taskbar_stashed_handle_dark_color"
+        android:clipToOutline="true"
+        android:layout_gravity="bottom"/>
+
+</com.android.launcher3.taskbar.TaskbarDragLayer>
\ No newline at end of file
diff --git a/quickstep/res/layout/customizable_taskbar_view.xml b/quickstep/res/layout/customizable_taskbar_view.xml
new file mode 100644
index 0000000..f55e1d0
--- /dev/null
+++ b/quickstep/res/layout/customizable_taskbar_view.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="@id/guideline1"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.1" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.5" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline3"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.7" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline4"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.9" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index 9144c7f..0551c12 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -14,7 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<TextView
+<com.android.quickstep.views.DigitalWellBeingToast
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     style="@style/TextTitle"
@@ -24,7 +24,8 @@
     android:forceHasOverlappingRendering="false"
     android:gravity="center"
     android:importantForAccessibility="noHideDescendants"
-    android:textColor="?androidprv:attr/materialColorOnSecondaryFixed"
+    android:textColor="?attr/materialColorOnSecondaryFixed"
     android:textSize="14sp"
     android:autoSizeTextType="uniform"
-    android:autoSizeMaxTextSize="14sp"/>
\ No newline at end of file
+    android:autoSizeMaxTextSize="14sp"
+    android:visibility="gone"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_step_menu.xml b/quickstep/res/layout/gesture_tutorial_step_menu.xml
index 668a2e1..7feb882 100644
--- a/quickstep/res/layout/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout/gesture_tutorial_step_menu.xml
@@ -20,7 +20,7 @@
     android:theme="@style/GestureTutorialActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="?androidprv:attr/materialColorSurfaceContainer"
+    android:background="?attr/materialColorSurfaceContainer"
     android:fitsSystemWindows="true">
 
     <androidx.constraintlayout.widget.ConstraintLayout
@@ -161,7 +161,7 @@
             android:layout_marginVertical="16dp"
             android:text="@string/gesture_tutorial_action_button_label"
             android:background="@drawable/gesture_tutorial_action_button_background"
-            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:backgroundTint="?attr/materialColorPrimary"
             android:stateListAnimator="@null"
 
             app:layout_constraintTop_toBottomOf="@id/guideline"
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index fb9bf99..36ece2a 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -26,7 +26,7 @@
     android:importantForAccessibility="no"
     android:autoMirrored="true"
     android:elevation="@dimen/task_thumbnail_icon_menu_elevation"
-    android:background="?androidprv:attr/materialColorSurfaceBright">
+    android:background="?attr/materialColorSurfaceBright">
 
     <!-- ignoring warning because the user of the anchor is a Rect where RTL is not needed -->
     <!-- This anchor's bounds is in the expected location after rotations and translations are
diff --git a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
similarity index 72%
rename from quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
rename to quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
index e48794e..c3f9e54 100644
--- a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
@@ -18,17 +18,20 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
-    android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:clipToOutline="true"
     android:importantForAccessibility="yes"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:focusBorderRadius="@dimen/keyboard_quick_switch_text_button_radius">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="@drawable/keyboard_quick_switch_overview_button_background"
+        android:layout_width="@dimen/keyboard_quick_switch_text_button_width"
+        android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+        android:background="@drawable/keyboard_quick_switch_text_button_background"
+        android:backgroundTint="?androidprv:attr/materialColorSurfaceContainer"
+        android:paddingHorizontal="@dimen/keyboard_quick_switch_text_button_horizontal_padding"
 
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -37,10 +40,11 @@
 
         <ImageView
             android:id="@+id/icon"
-            android:layout_width="@dimen/keyboard_quick_switch_recents_icon_size"
-            android:layout_height="@dimen/keyboard_quick_switch_recents_icon_size"
-            android:layout_marginBottom="8dp"
+            android:layout_width="@dimen/keyboard_quick_switch_desktop_icon_size"
+            android:layout_height="@dimen/keyboard_quick_switch_desktop_icon_size"
+            android:layout_marginBottom="4dp"
             android:tint="?androidprv:attr/materialColorOnSurface"
+            android:src="@drawable/ic_desktop"
 
             app:layout_constraintVertical_chainStyle="packed"
             app:layout_constraintTop_toTopOf="parent"
@@ -49,9 +53,9 @@
             app:layout_constraintEnd_toEndOf="parent"/>
 
         <TextView
-            style="@style/KeyboardQuickSwitchText"
+            style="@style/KeyboardQuickSwitchText.OnTaskView"
             android:id="@+id/text"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:textAlignment="center"
 
diff --git a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_overview_taskview.xml
similarity index 64%
copy from quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
copy to quickstep/res/layout/keyboard_quick_switch_overview_taskview.xml
index e48794e..0b44a2a 100644
--- a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_overview_taskview.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2023 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,44 +18,47 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
-    android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:clipToOutline="true"
     android:importantForAccessibility="yes"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:focusBorderRadius="@dimen/keyboard_quick_switch_text_button_radius">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="@drawable/keyboard_quick_switch_overview_button_background"
+        android:layout_width="@dimen/keyboard_quick_switch_text_button_width"
+        android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+        android:background="@drawable/keyboard_quick_switch_text_button_background"
+        android:backgroundTint="?androidprv:attr/materialColorSurfaceBright"
+        android:paddingHorizontal="@dimen/keyboard_quick_switch_text_button_horizontal_padding"
 
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent">
 
-        <ImageView
-            android:id="@+id/icon"
-            android:layout_width="@dimen/keyboard_quick_switch_recents_icon_size"
-            android:layout_height="@dimen/keyboard_quick_switch_recents_icon_size"
-            android:layout_marginBottom="8dp"
-            android:tint="?androidprv:attr/materialColorOnSurface"
-
-            app:layout_constraintVertical_chainStyle="packed"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toTopOf="@id/text"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
         <TextView
-            style="@style/KeyboardQuickSwitchText"
-            android:id="@+id/text"
+            style="@style/KeyboardQuickSwitchText.LargeText"
+            android:id="@+id/large_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textAlignment="center"
 
-            app:layout_constraintTop_toBottomOf="@id/icon"
+            app:layout_constraintVertical_chainStyle="packed"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/small_text"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+        <TextView
+            style="@style/KeyboardQuickSwitchText.OnTaskView"
+            android:id="@+id/small_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textAlignment="center"
+
+            app:layout_constraintTop_toBottomOf="@id/large_text"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toEndOf="parent"/>
diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
index c0ace9a..41eb623 100644
--- a/quickstep/res/layout/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
@@ -18,17 +18,17 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
-    android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:importantForAccessibility="yes"
     android:background="@drawable/keyboard_quick_switch_task_view_background"
     android:clipToOutline="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
+        android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
 
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -52,7 +52,7 @@
             android:layout_width="match_parent"
             android:layout_height="0dp"
             android:visibility="gone"
-            android:layout_marginTop="@dimen/keyboard_quick_switch_split_view_spacing"
+            android:layout_marginTop="@dimen/keyboard_quick_switch_view_small_spacing"
 
             app:layout_constraintTop_toBottomOf="@id/thumbnail_1"
             app:layout_constraintBottom_toBottomOf="parent"
diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview_square.xml b/quickstep/res/layout/keyboard_quick_switch_taskview_square.xml
new file mode 100644
index 0000000..1474949
--- /dev/null
+++ b/quickstep/res/layout/keyboard_quick_switch_taskview_square.xml
@@ -0,0 +1,90 @@
+<?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.
+-->
+<com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:importantForAccessibility="yes"
+    android:background="@drawable/keyboard_quick_switch_task_view_background"
+    android:clipToOutline="true"
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/content"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
+
+        app:layout_constraintDimensionRatio="1:1"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent">
+
+        <include
+            layout="@layout/keyboard_quick_switch_taskview_thumbnail"
+            android:id="@+id/thumbnail_1"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/thumbnail_2"/>
+
+        <include
+            layout="@layout/keyboard_quick_switch_taskview_thumbnail"
+            android:id="@+id/thumbnail_2"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:visibility="gone"
+            android:layout_marginStart="@dimen/keyboard_quick_switch_view_small_spacing"
+
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@id/thumbnail_1"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+        <ImageView
+            android:id="@+id/icon_1"
+            android:layout_width="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:layout_height="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:importantForAccessibility="no"
+            android:scaleType="centerCrop"
+
+            app:layout_constraintTop_toTopOf="@id/thumbnail_1"
+            app:layout_constraintBottom_toBottomOf="@id/thumbnail_1"
+            app:layout_constraintStart_toStartOf="@id/thumbnail_1"
+            app:layout_constraintEnd_toEndOf="@id/thumbnail_1"/>
+
+        <ImageView
+            android:id="@+id/icon_2"
+            android:layout_width="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:layout_height="@dimen/keyboard_quick_switch_taskview_icon_size"
+            android:importantForAccessibility="no"
+            android:visibility="gone"
+            android:scaleType="centerCrop"
+
+            app:layout_constraintTop_toTopOf="@id/thumbnail_2"
+            app:layout_constraintBottom_toBottomOf="@id/thumbnail_2"
+            app:layout_constraintStart_toStartOf="@id/thumbnail_2"
+            app:layout_constraintEnd_toEndOf="@id/thumbnail_2"/>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView>
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 2bba788..2420a46 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -43,7 +43,7 @@
             android:layout_height="@dimen/keyboard_quick_switch_no_recent_items_icon_size"
             android:layout_marginBottom="@dimen/keyboard_quick_switch_no_recent_items_icon_margin"
             android:src="@drawable/view_carousel"
-            android:tint="?androidprv:attr/materialColorOnSurface"
+            android:tint="?attr/materialColorOnSurface"
             android:importantForAccessibility="no"
 
             app:layout_constraintVertical_chainStyle="packed"
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index 3380ea4..40f38f4 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -23,6 +23,6 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="@string/recents_clear_all"
-    android:textColor="?androidprv:attr/materialColorOnSurface"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    android:textColor="?attr/materialColorOnSurface"
+    launcher:focusBorderColor="?attr/materialColorOutline"
     android:textSize="14sp" />
\ No newline at end of file
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index 797ea45..a11974c 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -41,5 +41,6 @@
         android:textColor="?androidprv:attr/textColorOnAccent"
         android:layout_marginStart="@dimen/split_instructions_start_margin_cancel"
         android:text="@string/toast_split_select_app_cancel"
+        android:textStyle="bold"
         android:visibility="gone"/>
 </com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 46c1332..760bcdb 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -19,14 +19,14 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/task"
+    android:id="@+id/task_view_single"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
-    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
+    launcher:focusBorderColor="?attr/materialColorOutline"
+    launcher:hoverBorderColor="?attr/materialColorPrimary">
 
     <include layout="@layout/task_thumbnail_deprecated" />
 
@@ -47,4 +47,7 @@
         android:inflatedId="@id/icon"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content" />
+
+    <include layout="@layout/digital_wellbeing_toast"
+        android:id="@+id/digital_wellbeing_toast"/>
 </com.android.quickstep.views.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 453057c..0472007 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
      Copyright (C) 2022 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,22 +14,16 @@
      limitations under the License.
 -->
 
-<com.android.quickstep.views.DesktopTaskView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+<com.android.quickstep.views.DesktopTaskView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/task"
+    android:id="@+id/task_view_desktop"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:clipChildren="true"
+    android:contentDescription="@string/recent_task_desktop"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
-    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary"
-    android:clipToPadding="true"
-    android:padding="0.1dp">
-    <!-- Setting a padding of 0.1 dp since android:clipToPadding needs a non-zero value for
-    padding to work-->
+    launcher:focusBorderColor="?attr/materialColorOutline"
+    launcher:hoverBorderColor="?attr/materialColorPrimary">
     <View
         android:id="@+id/background"
         android:layout_width="match_parent"
@@ -38,8 +31,13 @@
 
     <ViewStub
         android:id="@+id/icon"
-        android:inflatedId="@id/icon"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_width="wrap_content" />
+        android:inflatedId="@id/icon" />
+
+    <com.android.quickstep.views.DesktopTaskContentView
+        android:id="@+id/desktop_content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
 
 </com.android.quickstep.views.DesktopTaskView>
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 708aa3c..c36a45e 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -24,14 +24,14 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/task"
+    android:id="@+id/task_view_grouped"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
-    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
+    launcher:focusBorderColor="?attr/materialColorOutline"
+    launcher:hoverBorderColor="?attr/materialColorPrimary">
 
     <include layout="@layout/task_thumbnail_deprecated"/>
 
@@ -73,4 +73,10 @@
         android:inflatedId="@id/bottomRight_icon"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content" />
+
+    <include layout="@layout/digital_wellbeing_toast"
+        android:id="@+id/digital_wellbeing_toast"/>
+
+    <include layout="@layout/digital_wellbeing_toast"
+        android:id="@+id/bottomRight_digital_wellbeing_toast"/>
 </com.android.quickstep.views.GroupedTaskView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index 34640e6..afbcdb5 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -15,21 +15,24 @@
 -->
 <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">
+    android:layout_height="match_parent" >
 
-    <ImageView
+    <com.android.quickstep.views.FixedSizeImageView
         android:id="@+id/task_thumbnail"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:importantForAccessibility="no"
-        android:visibility="gone"/>
+        android:scaleType="matrix"
+        android:visibility="invisible"/>
 
     <com.android.quickstep.task.thumbnail.LiveTileView
         android:id="@+id/task_thumbnail_live_tile"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:visibility="gone"/>
+        android:visibility="invisible"/>
 
     <View
         android:id="@+id/task_thumbnail_scrim"
@@ -38,4 +41,23 @@
         android:background="@color/overview_foreground_scrim_color"
         android:alpha="0" />
 
+    <View
+        android:id="@+id/splash_background"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/black"
+        android:alpha="0"
+        android:importantForAccessibility="no" />
+
+    <com.android.quickstep.views.FixedSizeImageView
+        android:id="@+id/splash_icon"
+        android:layout_width="@dimen/task_thumbnail_splash_icon_size"
+        android:layout_height="@dimen/task_thumbnail_splash_icon_size"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:scaleType="fitCenter"
+        android:alpha="0"
+        android:importantForAccessibility="no" />
 </com.android.quickstep.task.thumbnail.TaskThumbnailView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_view_menu_option.xml b/quickstep/res/layout/task_view_menu_option.xml
index ffe2401..5218de0 100644
--- a/quickstep/res/layout/task_view_menu_option.xml
+++ b/quickstep/res/layout/task_view_menu_option.xml
@@ -31,7 +31,7 @@
       android:layout_height="@dimen/system_shortcut_icon_size"
       android:layout_marginStart="@dimen/task_menu_option_start_margin"
       android:layout_gravity="center_horizontal"
-      android:backgroundTint="?androidprv:attr/materialColorOnSurface"/>
+      android:backgroundTint="?attr/materialColorOnSurface"/>
 
     <TextView
         style="@style/BaseIcon"
@@ -40,7 +40,7 @@
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/task_menu_option_text_start_margin"
         android:textSize="14sp"
-        android:textColor="?androidprv:attr/materialColorOnSurface"
+        android:textColor="?attr/materialColorOnSurface"
         android:focusable="false"
         android:gravity="start"
         android:ellipsize="end" />
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index 736706a..54f9ae8 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -35,6 +35,25 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
+    <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"
         android:layout_width="match_parent"
diff --git a/quickstep/res/layout/taskbar_overflow_view.xml b/quickstep/res/layout/taskbar_overflow_view.xml
new file mode 100644
index 0000000..7444e59
--- /dev/null
+++ b/quickstep/res/layout/taskbar_overflow_view.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.
+-->
+
+<!-- Note: The actual size will match the taskbar icon sizes in TaskbarView#onLayout(). -->
+<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"
+    android:contentDescription="@string/taskbar_overflow_a11y_title" />
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 73c8129..eac8043 100644
--- a/quickstep/res/values-af/strings.xml
+++ b/quickstep/res/values-af/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Vee alles uit"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Stoor app-paar"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tik op ’n ander app om verdeelde skerm te gebruik"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Kies ’n ander app as jy verdeelde skerm wil gebruik"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Kanselleer"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Kanselleer"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Verlaat verdeeldeskermkeuse"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Kies nog ’n app as jy verdeelde skerm wil gebruik"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Jou organisasie laat nie hierdie program toe nie"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Kitsinstellings"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taakbalk"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taakbalk word gewys"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taakbalk en borrels wys links"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taakbalk en borrels wys regs"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taakbalk is versteek"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taakbalk en borrels is versteek"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigasiebalk"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Wys altyd Taakbalk"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Verander navigasiemodus"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taakbalkverdeler"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taakbalkoorloop"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Skuif na links bo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Skuif na regs onder"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Wys nog # app.}other{Wys nog # apps.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Wys # rekenaarapp.}other{Wys # rekenaarapps.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{meer app}other{meer apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Werkskerm"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> en <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Borrel"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Oorvloei"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> vanaf <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> en nog <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Skuif links"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Skuif regs"</string>
+    <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 f9eed39..b9ee381 100644
--- a/quickstep/res/values-am/strings.xml
+++ b/quickstep/res/values-am/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ሁሉንም አጽዳ"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"የመተግበሪያ ጥምረትን አስቀምጥ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"የተከፈለ ማያ ገጽን ለመጠቀም ሌላ መተግበሪያ መታ ያድርጉ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"የተከፈለ ማያ ገጽን ለመጠቀም ሌላ መተግበሪያ ይምረጡ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ይቅር"</b></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="blocked_by_policy" msgid="2071401072261365546">"ይህ ድርጊት በመተግበሪያው ወይም በእርስዎ ድርጅት አይፈቀድም"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ፈጣን ቅንብሮች"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"የተግባር አሞሌ"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"የተግባር አሞሌ ይታያል"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"የተግባር አሞሌ እና አረፋዎች በግራ በኩል ይታያሉ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"የተግባር አሞሌ እና አረፋዎች በቀኝ በኩል ይታያሉ"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"የተግባር አሞሌ ተደብቋል"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"የተግባር አሞሌ እና አረፋዎች ተደብቀዋል"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"የአሰሳ አሞሌ"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ሁልጊዜ የተግባር አሞሌ ያሳዩ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"የአሰሳ ሁነታን ይለውጡ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"የተግባር አሞሌ አካፋይ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"የተግባር አሞሌ ትርፍ ፍሰት"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ወደ ላይ/ግራ ይውሰዱ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ወደ ታች/ቀኝ ይውሰዱ"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ተጨማሪ # መተግበሪያ አሳይ።}one{ተጨማሪ # መተግበሪያ አሳይ።}other{ተጨማሪ # መተግበሪያዎች አሳይ።}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# የዴስክቶፕ መተግበሪያ አሳይ።}one{# የዴስክቶፕ መተግበሪያ አሳይ።}other{# የዴስክቶፕ መተግበሪያዎች አሳይ።}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ተጨማሪ መተግበሪያ}one{ተጨማሪ መተግበሪያ}other{ተጨማሪ መተግበሪያዎች}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ዴስክቶፕ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> እና <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"አረፋ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ትርፍ ፍሰት"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ከ<xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> እና <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ተጨማሪ"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ወደ ግራ ያንቀሳቅሱ"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ወደ ቀኝ ያንቀሳቅሱ"</string>
+    <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 7a0be9b..b699d93 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"محو الكل"</string>
@@ -88,7 +90,7 @@
     <string name="gesture_tutorial_nice" msgid="2936275692616928280">"أحسنت"</string>
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"الدليل التوجيهي <xliff:g id="CURRENT">%1$d</xliff:g> من إجمالي <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"اكتملت عملية الإعداد"</string>
-    <string name="allset_hint" msgid="459504134589971527">"يمكنك التمرير سريعًا إلى الأعلى للانتقال إلى الشاشة الرئيسية"</string>
+    <string name="allset_hint" msgid="459504134589971527">"مرّر سريعًا للأعلى للانتقال إلى الشاشة الرئيسية"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"انقر على زر الشاشة الرئيسية للانتقال إلى الشاشة الرئيسية."</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"يمكنك الآن بدء استخدام <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"الجهاز"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"حفظ استخدام التطبيقين معًا"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"انقر على تطبيق آخر لاستخدام وضع تقسيم الشاشة."</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"اختَر تطبيقًا آخر لاستخدام \"وضع تقسيم الشاشة\""</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"إلغاء"</b></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="blocked_by_policy" msgid="2071401072261365546">"لا يسمح التطبيق أو لا تسمح مؤسستك بهذا الإجراء."</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"إعدادات سريعة"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"شريط التطبيقات"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"تم إظهار شريط التطبيقات"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"يتم عرض \"شريط التطبيقات\" و\"فقاعات المحادثات\" يمينًا"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"يتم عرض \"شريط التطبيقات\" و\"فقاعات المحادثات\" يسارًا"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"تم إخفاء شريط التطبيقات"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"تم إخفاء \"شريط التطبيقات\" و\"فقاعات المحادثات\""</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"شريط التنقل"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"إظهار شريط التطبيقات دائمًا"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"تغيير وضع التنقل"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"مقسِّم شريط التطبيقات"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"القائمة الكاملة لشريط التطبيقات"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"الانتقال إلى يمين الشاشة أو أعلاها"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"الانتقال إلى يسار الشاشة أو أسفلها"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{إظهار تطبيق واحد آخر}zero{إظهار # تطبيق آخر}two{إظهار تطبيقَين آخرَين}few{إظهار # تطبيقات أخرى}many{إظهار # تطبيقًا آخر}other{إظهار # تطبيق آخر}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{عرض تطبيق واحد متوافق مع الكمبيوتر المكتبي}zero{عرض # تطبيق متوافق مع الكمبيوتر المكتبي}two{عرض تطبيقَين متوافقين مع الكمبيوتر المكتبي}few{عرض # تطبيقات متوافقة مع الكمبيوتر المكتبي}many{عرض # تطبيقًا متوافقًا مع الكمبيوتر المكتبي}other{عرض # تطبيق متوافق مع الكمبيوتر المكتبي}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{تطبيق واحد آخر}zero{تطبيق آخر}two{تطبيقان آخران}few{تطبيقات أخرى}many{تطبيقًا آخر}other{تطبيق آخر}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"وضع الكمبيوتر المكتبي"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"\"<xliff:g id="APP_NAME_1">%1$s</xliff:g>\" و\"<xliff:g id="APP_NAME_2">%2$s</xliff:g>\""</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"فقاعة"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"القائمة الكاملة"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"‫\"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" من \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"‫\"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>\" و<xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> غيرها"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"نقل لليسار"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"نقل لليمين"</string>
+    <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 d440400..7599530 100644
--- a/quickstep/res/values-as/strings.xml
+++ b/quickstep/res/values-as/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"আটাইবোৰ মচক"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"এপৰ পেয়াৰ ছেভ কৰক"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপত টিপক"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপ্ বাছনি কৰক"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"বাতিল কৰক"</b></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="blocked_by_policy" msgid="2071401072261365546">"এপ্‌টোৱে অথবা আপোনাৰ প্ৰতিষ্ঠানে এই কাৰ্যটোৰ অনুমতি নিদিয়ে"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ক্ষিপ্ৰ ছেটিং"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"টাস্কবাৰ"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"টাস্কবাৰ দেখুওৱা হৈছে"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"টাস্কবাৰ আৰু বাবল বাওঁফালে দেখুওৱা হৈছে"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"টাস্কবাৰ আৰু বাবল সোঁফালে দেখুওৱা হৈছে"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"টাস্কবাৰ লুকুৱাই থোৱা হৈছে"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"টাস্কবাৰ আৰু বাবল লুকুওৱা হৈছে"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"নেভিগেশ্বনৰ দণ্ড"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"টাস্কবাৰ সদায় দেখুৱাওক"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"নেভিগেশ্বন ম’ড সলনি কৰক"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"টাস্কবাৰ বিভাজক"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"টাস্কবাৰ অ’ভাৰফ্ল"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ওপৰৰ বাঁওফাললৈ নিয়ক"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"তলৰ সোঁফাললৈ নিয়ক"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{আৰু # টা এপ্‌ দেখুৱাওক।}one{আৰু # টা এপ্‌ দেখুৱাওক।}other{আৰু # টা এপ্‌ দেখুৱাওক।}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# টা ডেস্কটপ এপ্ দেখুৱাওক।}one{# টা ডেস্কটপ এপ্ দেখুৱাওক।}other{# টা ডেস্কটপ এপ্ দেখুৱাওক।}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{অধিক এপ্‌}one{অধিক এপ্‌}other{অধিক এপ্‌}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ডেস্কটপ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> আৰু <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"বাবল"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"অ’ভাৰফ্ল’"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>ৰ পৰা <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> আৰু <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> টা"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"বাওঁফাললৈ নিয়ক"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"সোঁফাললৈ নিয়ক"</string>
+    <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 9cdcc01..6c5748d 100644
--- a/quickstep/res/values-az/strings.xml
+++ b/quickstep/res/values-az/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Hamısını silin"</string>
@@ -67,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>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Tətbiq cütünü saxla"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Bölünmüş ekran üçün başqa tətbiqə toxunun"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Bölünmüş ekrandan istifadə üçün başqa tətbiq seçin"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Ləğv edin"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Ləğv edin"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Bölünmüş ekran seçimindən çıxın"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Bölünmüş ekrandan istifadə üçün başqa tətbiq seçin"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Bu əməliyyata tətbiq və ya təşkilatınız tərəfindən icazə verilmir"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Sürətli Ayarlar"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Tapşırıq paneli"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"İşləmə paneli göstərilir"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"İşləmə paneli, yumrucuqlar solda göstərilir"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"İşləmə paneli, yumrucuqlar sağda göstərilir"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"İşləmə paneli gizlədilib"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"İşləmə paneli, yumrucuqlar gizlədilib"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Naviqasiya paneli"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"İşləmə paneli həmişə görünsün"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Naviqasiya rejimini dəyişin"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"İşləmə paneli ayırıcısı"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tapşırıqlar Paneli üzrə əlavə menyu"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Yuxarı/sola köçürün"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Aşağı/sağa köçürün"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Daha # tətbiqi göstərin.}other{Daha # tətbiqi göstərin.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# masaüstü tətbiqini göstərin.}other{# masaüstü tətbiqini göstərin.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{əlavə tətbiq}other{əlavə tətbiq}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Masaüstü"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> və <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Yumrucuq"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Kənara çıxma"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> və daha <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> yumrucuq"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Sola köçürün"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Sağa köçürün"</string>
+    <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 b6271a9..cbcffdf 100644
--- a/quickstep/res/values-b+sr+Latn/strings.xml
+++ b/quickstep/res/values-b+sr+Latn/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Obriši sve"</string>
@@ -89,7 +91,7 @@
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"Vodič <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"Gotovo!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Prevucite nagore da biste otvorili početni ekran"</string>
-    <string name="allset_button_hint" msgid="2395219947744706291">"Dodirnite dugme Početak da bisti išli na početni ekran"</string>
+    <string name="allset_button_hint" msgid="2395219947744706291">"Dodirnite dugme Početak da biste otišli na početni ekran"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Spremni ste da počnete da koristite <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"uređaj"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Podešavanja kretanja kroz sistem"</annotation></string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Sačuvaj par aplikacija"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu aplikaciju za podeljeni ekran"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Odaberite drugu aplikaciju da biste koristili podeljeni ekran"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Otkaži"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Otkaži"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izlazak iz biranja podeljenog ekrana"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu aplikaciju za podeljeni ekran"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ili organizacija ne dozvoljavaju ovu radnju"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Brza podešavanja"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Traka zadataka"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Traka zadataka je prikazana"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Prikaz zadataka/oblačića levo"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Prikaz zadataka/oblačića desno"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Traka zadataka je skrivena"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Skriveni zadaci/oblačići"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Traka za navigaciju"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvek prikazuj traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promeni režim navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdelnik trake zadataka"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Preklopna traka zadataka"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premesti gore levo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premesti dole desno"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Prikaži još # aplikaciju.}one{Prikaži još # aplikaciju.}few{Prikaži još # aplikacije.}other{Prikaži još # aplikacija.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Prikaži # aplikaciju za računare.}one{Prikaži # aplikaciju za računare.}few{Prikaži # aplikacije za računare.}other{Prikaži # aplikacija za računare.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Računar"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Oblačić"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Preklopni"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Pomeri nalevo"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Pomeri nadesno"</string>
+    <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 bb6c764..103e243 100644
--- a/quickstep/res/values-be/strings.xml
+++ b/quickstep/res/values-be/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Ачысціць усё"</string>
@@ -39,7 +41,7 @@
     <string name="hotseat_edu_accept" msgid="1611544083278999837">"Атрымліваць прапановы праграм"</string>
     <string name="hotseat_edu_dismiss" msgid="2781161822780201689">"Не, дзякуй"</string>
     <string name="hotseat_prediction_settings" msgid="6246554993566070818">"Налады"</string>
-    <string name="hotseat_auto_enrolled" msgid="522100018967146807">"Тут з\'яўляюцца праграмы, якімі вы карыстаецеся найбольш часта. Гэты спіс змяняецца на падставе вашых дзеянняў"</string>
+    <string name="hotseat_auto_enrolled" msgid="522100018967146807">"Тут з’яўляюцца праграмы, якімі вы карыстаецеся найбольш часта. Гэты спіс змяняецца на падставе вашых дзеянняў"</string>
     <string name="hotseat_tip_no_empty_slots" msgid="1325212677738179185">"Перацягніце праграмы з ніжняга радка, каб атрымліваць прапановы праграм"</string>
     <string name="hotseat_tip_gaps_filled" msgid="3035673010274223538">"Прапановы праграм дададзены на свабоднае месца"</string>
     <string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"Прапановы праграм уключаны"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Захаваць спалучэнне"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Каб падзяліць экран, націсніце на іншую праграму"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Каб карыстацца рэжымам падзеленага экрана, выберыце другую праграму"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Скасаваць"</b></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="blocked_by_policy" msgid="2071401072261365546">"Гэта дзеянне не дазволена праграмай ці вашай арганізацыяй"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Хуткія налады"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Панэль задач"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Панэль задач паказана"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Панэль задач і ўсплывальныя чаты паказваюцца злева"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Панэль задач і ўсплывальныя чаты паказваюцца справа"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Панэль задач схавана"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Панэль задач і ўсплывальныя чаты схаваны"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Панэль навігацыі"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Заўсёды паказваць панэль задач"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Змяніць рэжым навігацыі"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Раздзяляльнік панэлі задач"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Меню з пашырэннем панэлі задач"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Перамясціць уверх/улева"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перамясціць уніз/управа"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Паказаць ячшэ # праграму.}one{Паказаць ячшэ # праграму.}few{Паказаць ячшэ # праграмы.}many{Паказаць ячшэ # праграм.}other{Паказаць ячшэ # праграмы.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Паказаць # праграму для ПК.}one{Паказаць # праграму для ПК.}few{Паказаць # праграмы для ПК.}many{Паказаць # праграм для ПК.}other{Паказаць # праграмы для ПК.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{даступная праграма}one{даступная праграма}few{даступныя праграмы}many{даступных праграм}other{даступнай праграмы}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Працоўны стол"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> і <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Бурбалкі"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Меню з пашырэннем"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, крыніца: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> і яшчэ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Перамясціць улева"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Перамясціць управа"</string>
+    <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 d674dbc..d624914 100644
--- a/quickstep/res/values-bg/strings.xml
+++ b/quickstep/res/values-bg/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Изчистване на всички"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Запис на двойка приложения"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Докоснете друго прил., за да ползвате разд. екран"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"За разделен екран изберете още едно приложение"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Отказ"</b></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="blocked_by_policy" msgid="2071401072261365546">"Това действие не е разрешено от приложението или организацията ви"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Бързи настройки"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Лента на задачите"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Лентата на задачите се показва"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Лентата и балончетата са вляво"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Лентата и балончетата са вдясно"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Лентата на задачите е скрита"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Лентата и балончетата са скрити"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Лента за навигация"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Лентата на задачите винаги да се показва"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Промяна на режима на навигация"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделител на лентата на задачите"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Меню при препълване на лентата на задачите"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Преместване горе/вляво"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Преместване долу/вдясно"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Показване на още # приложение.}other{Показване на още # приложения.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Показване на # настолно приложение.}other{Показване на # настолни приложения.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{допълнително приложение}other{допълнителни приложения}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Режим за настолни компютри"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Балонче"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Препълване"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> от <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и още <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Преместване наляво"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Преместване надясно"</string>
+    <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 3e974f5..c7bc2cf 100644
--- a/quickstep/res/values-bn/strings.xml
+++ b/quickstep/res/values-bn/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"সবকিছু খালি করুন"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"অ্যাপ পেয়ার সেভ করুন"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"স্প্লিট স্ক্রিন ব্যবহারের জন্য অ্যাপে ট্যাপ করুন"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"স্প্লিট স্ক্রিন ব্যবহার করতে অন্য অ্যাপ বেছে নিন"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"বাতিল করুন"</b></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="blocked_by_policy" msgid="2071401072261365546">"এই অ্যাপ বা আপনার প্রতিষ্ঠান এই অ্যাকশনটি পারফর্ম করার অনুমতি দেয়নি"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"দ্রুত সেটিংস"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"টাস্কবার"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"\'টাস্কবার\' দেখানো হয়েছে"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"টাস্কবার ও বাবল বাঁদিকে দেখানো হয়েছে"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"টাস্কবার ও বাবল ডানদিকে দেখানো হয়েছে"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"\'টাস্কবার\' লুকানো রয়েছে"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"টাস্কবার ও বাবল লুকানো হয়েছে"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"নেভিগেশন বার"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"সবসময় টাস্কবার দেখুন"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"\'নেভিগেশন\' মোড পরিবর্তন করুন"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"টাস্কবার ডিভাইডার"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"টাস্কবার ওভারফ্লো"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"উপরে/বাঁদিকে সরান"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"নিচে/ডানদিকে সরান"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{আরও #টি অ্যাপ দেখুন।}one{আরও #টি অ্যাপ দেখুন।}other{আরও #টি অ্যাপ দেখুন।}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{#টি ডেস্কটপ অ্যাপ দেখুন।}one{#টি ডেস্কটপ অ্যাপ দেখুন।}other{#টি ডেস্কটপ অ্যাপ দেখুন।}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{আরও অ্যাপ}one{আরও অ্যাপ}other{আরও অ্যাপ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ডেস্কটপ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ও <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"বাবল"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ওভারফ্লো"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> থেকে <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> এবং আরও <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>টি"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"বাঁদিকে সরান"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ডানদিকে সরান"</string>
+    <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 19268be..cea1921 100644
--- a/quickstep/res/values-bs/strings.xml
+++ b/quickstep/res/values-bs/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Obriši sve"</string>
@@ -47,7 +49,7 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Predviđena aplikacija: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Rotirajte uređaj"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Rotirajte uređaj da završite vodič za navigaciju pokretima"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Prevucite s krajnjeg desnog ili krajnjeg lijevog ruba"</string>
+    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Prevucite s krajnjeg desnog ili lijevog ruba"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Prevucite s desnog ili lijevog ruba prema sredini ekrana i pustite"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Naučili ste kako prevući zdesna da se vratite. Sljedeće naučite kako prebacivati između aplikacija."</string>
     <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Savladali ste pokret za vraćanje. Sljedeće naučite kako prebacivati između aplikacija."</string>
@@ -73,7 +75,7 @@
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Vodite računa da prevučete s donjeg ruba ekrana prema gore"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Pokušajte zadržati prozor duže prije puštanja"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Prevucite ravno nagore, a zatim zastanite"</string>
-    <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Naučili ste kako koristiti pokrete. Idite u Postavke da isključite pokrete."</string>
+    <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Naučili ste kako koristiti pokrete. Da ih isključite, idite u Postavke."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Savladali ste pokret za prebacivanje između aplikacija"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Prevucite da prebacujete između aplikacija"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Da se prebacujete između aplikacija, prevucite s dna ekrana nagore, zadržite, a zatim pustite."</string>
@@ -89,17 +91,17 @@
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"Vodič <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"Sve je spremno!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Prevucite prema gore da odete na početni ekran"</string>
-    <string name="allset_button_hint" msgid="2395219947744706291">"Dodirnite dugme za početni ekran da odete napočetni ekran"</string>
+    <string name="allset_button_hint" msgid="2395219947744706291">"Dodirnite dugme za početni ekran da odete na početni ekran"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Spremni ste da počnete koristiti <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"uređaj"</string>
-    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Postavke navigiranja sistemom"</annotation></string>
+    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Postavke navigacije sistemom"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Dijeli"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Snimak ekrana"</string>
     <string name="action_split" msgid="2098009717623550676">"Podijeli"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"Sačuvaj par aplikacija"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu apl. da koristite podijeljeni ekran"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Odaberite drugu aplikaciju da koristite podijeljeni ekran"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Otkaži"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Otkaži"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izlaz iz odabira podijeljenog ekrana"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu apl. da koristite podijeljeni ekran"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Ovu radnju ne dozvoljava aplikacija ili vaša organizacija"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Brze postavke"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Traka zadataka"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Traka zadataka je prikazana"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Traka zadataka/oblačići prik. lijevo"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Traka zadataka/oblačići prik. desno"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Traka zadataka je sakrivena"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Traka zadataka/oblačići sakriveni"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigaciona traka"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvijek prikaži traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promijeni način navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdjelnik trake zadataka"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Preklopni meni trake zadataka"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premjesti gore lijevo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje desno"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Prikaži još # aplikaciju.}one{Prikaži još # aplikaciju.}few{Prikaži još # aplikacije.}other{Prikaži još # aplikacija.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Prikaži # aplikaciju za računar.}one{Prikaži # aplikaciju za računar.}few{Prikaži # aplikacije za računar.}other{Prikaži # aplikacija za računar.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Računar"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Oblačić"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Preklopni meni"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Pomjeranje ulijevo"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Pomjeranje udesno"</string>
+    <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 6a6f131..e2352d7 100644
--- a/quickstep/res/values-ca/strings.xml
+++ b/quickstep/res/values-ca/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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">"Ordinador"</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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Esborra-ho tot"</string>
@@ -59,7 +61,7 @@
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Per tornar a la pantalla anterior, llisca amb dos dits des de l\'extrem esquerre o dret cap al centre de la pantalla."</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Torna enrere"</string>
     <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Llisca des de la vora esquerra o dreta cap al centre de la pantalla."</string>
-    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Assegura\'t de lliscar cap amunt des de la part inferior de la pantalla"</string>
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Assegura\'t de lliscar cap amunt des de la part inferior de la pantalla."</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Assegura\'t de no aturar-te abans de deixar anar."</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Assegura\'t de lliscar recte cap amunt."</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Has completat el gest per anar a la pantalla d\'inici. Ara, descobreix com pots tornar enrere."</string>
@@ -72,7 +74,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Ben fet!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Assegura\'t de lliscar cap amunt des de la part inferior de la pantalla."</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Prova de mantenir premuda la finestra durant més temps abans de deixar-la anar"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Assegura\'t de lliscar directament cap amunt i després aturar-te"</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Assegura\'t de lliscar directament cap amunt i després aturar-te."</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Has après a utilitzar els gestos. Per desactivar-los, ves a Configuració."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Has completat el gest per canviar d\'aplicació"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Llisca per canviar d\'aplicació"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Desa la parella d\'apps"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toca una altra app per utilitzar pantalla dividida"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Tria una altra aplicació per utilitzar la pantalla dividida"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancel·la"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancel·la"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Surt de la selecció de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Tria una altra app per utilitzar pantalla dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"L\'aplicació o la teva organització no permeten aquesta acció"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Config. ràpida"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barra de tasques"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Es mostra la Barra de tasques"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barra i bombolles a l\'esquerra"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barra i bombolles a la dreta"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"S\'ha amagat la Barra de tasques"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barra i bombolles amagades"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barra de navegació"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barra de tasques sempre visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Canvia el mode de navegació"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Separador de la Barra de tasques"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Menú addicional de la barra de tasques"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mou a la part superior o a l\'esquerra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mou a la part inferior o a la dreta"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostra # aplicació més.}other{Mostra # aplicacions més.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Mostra # aplicació per a ordinadors.}other{Mostra # aplicacions per a ordinadors.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicació més}other{aplicacions més}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Escriptori"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bombolla"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Desbordament"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> més"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Mou cap a l\'esquerra"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Mou cap a la dreta"</string>
+    <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 e134850..de550d1 100644
--- a/quickstep/res/values-cs/strings.xml
+++ b/quickstep/res/values-cs/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Vymazat vše"</string>
@@ -50,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>
@@ -62,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>
@@ -74,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>
@@ -91,7 +93,7 @@
     <string name="allset_hint" msgid="459504134589971527">"Přejetím nahoru se vrátíte na plochu"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Klepnutím na tlačítko plochy se vrátíte na plochu"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> můžete začít používat"</string>
-    <string name="default_device_name" msgid="6660656727127422487">"zařízení"</string>
+    <string name="default_device_name" msgid="6660656727127422487">"Zařízení"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Nastavení navigace v systému"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Sdílet"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Snímek obrazovky"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Uložit dvojici aplikací"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Obrazovku rozdělíte klepnutím na jinou aplikaci"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Výběrem další aplikace rozdělíte obrazovku"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Zrušit"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Zrušit"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Výběr opuštění rozdělené obrazovky"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Vyberte podporovanou aplikaci"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikace nebo organizace zakazuje tuto akci"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Rychlé nastavení"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Panel aplikací"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Panel aplikací je zobrazen"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Panel a bubliny vlevo zobr."</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Panel a bubliny vpravo zobr."</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Panel aplikací je skrytý"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Panel a bubliny jsou skryty"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigační panel"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vždy zobrazovat panel aplikací"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Změnit režim navigace"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Rozdělovač panelu aplikací"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Přetečení panelu aplikací"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Přesunout doleva nahoru"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Přesunout doprava dolů"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Zobrazit # další aplikaci.}few{Zobrazit # další aplikace.}many{Zobrazit # další aplikace.}other{Zobrazit # dalších aplikací.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Zobrazit # aplikaci pro počítač.}few{Zobrazit # aplikace pro počítač.}many{Zobrazit # aplikace pro počítač.}other{Zobrazit # aplikací pro počítač.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{další aplikace}few{další aplikace}many{další aplikace}other{dalších aplikací}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Počítač"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> a <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bublina"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Rozbalovací nabídka"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikace <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> a ještě <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Přesunout doleva"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Přesunout doprava"</string>
+    <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 e9cdbce..b022172 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Ryd alt"</string>
@@ -50,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>
@@ -62,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>
@@ -74,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>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Gem appsammenknytning"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tryk på en anden app for at bruge opdelt skærm"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Vælg en anden app for at bruge opdelt skærm"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Annuller"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Annuller"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Luk valg af opdelt skærm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Vælg en anden app for at bruge opdelt skærm"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller din organisation tillader ikke denne handling"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Kvikmenu"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Proceslinje"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Proceslinjen vises"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Proceslinje og bobler til venstre"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Proceslinje og bobler til højre"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Proceslinjen er skjult"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Proceslinje og bobler skjult"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigationslinje"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vis altid proceslinjen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Skift navigationstilstand"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Opdeling af proceslinjen"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Prikmenu på proceslinjen"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flyt til toppen eller venstre side"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flyt til bunden eller højre side"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Vis # app mere.}one{Vis # app mere.}other{Vis # apps mere.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Vis # computerprogram.}one{Vis # computerprogram.}other{Vis # computerprogrammer.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{yderligere app}one{yderligere app}other{yderligere apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Computer"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> og <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Boble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overløb"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> mere"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Flyt til venstre"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Flyt til højre"</string>
+    <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 9e5cb12..f70e408 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Alle Apps schließen"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"App-Paar speichern"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Für Splitscreen auf weitere App tippen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Für Splitscreen andere App auswählen"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Abbrechen"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Abbrechen"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Splitscreen-Auswahl beenden"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Für Splitscreen andere App auswählen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Die App oder deine Organisation lässt diese Aktion nicht zu"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Schnelleinstellungen"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taskleiste"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taskleiste eingeblendet"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taskleiste &amp; Bubbles links eingeblendet"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taskleiste &amp; Bubbles rechts eingeblendet"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taskleiste ausgeblendet"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taskleiste &amp; Bubbles ausgeblendet"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigationsleiste"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Taskleiste immer anzeigen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigationsmodus ändern"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskleisten-Teiler"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Dreipunkt-Menü der Taskleiste"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Nach oben / Nach links verschieben"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Nach unten / Nach rechts verschieben"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# weitere App anzeigen}other{# weitere Apps anzeigen}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# Desktop-App anzeigen.}other{# Desktop-Apps anzeigen.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{weitere App}other{weitere Apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktopmodus"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> und <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Weitere Optionen"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ aus <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> und <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> weitere"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Nach links bewegen"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Nach rechts bewegen"</string>
+    <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 bbb1282..d7ff2ad 100644
--- a/quickstep/res/values-el/strings.xml
+++ b/quickstep/res/values-el/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Διαγραφή όλων"</string>
@@ -67,7 +69,7 @@
     <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>
-    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Αρχική"</string>
+    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Μετάβαση στην αρχική οθόνη"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Σύρετε προς τα επάνω από το κάτω μέρος της οθόνης"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Μπράβο!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Φροντίστε να σύρετε προς τα επάνω από το κάτω άκρο της οθόνης"</string>
@@ -90,7 +92,7 @@
     <string name="allset_title" msgid="5021126669778966707">"Όλα έτοιμα!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Σύρετε προς τα επάνω για να μεταβείτε στην αρχική σελίδα"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Πατήστε το κουμπί αρχικής οθόνης για να μεταβείτε στην αρχική οθόνη"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"Είστε έτοιμοι να ξεκινήσετε να χρησιμοποιείτε το/τη <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"Είστε έτοιμοι να ξεκινήσετε να χρησιμοποιείτε τη <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"συσκευή"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Ρυθμίσεις πλοήγησης συστήματος"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Κοινοποίηση"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Αποθήκ. ζεύγ. εφαρμ."</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Πατήστε άλλη εφαρμογή για διαχωρισμό οθόνης"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Επιλέξτε άλλη εφαρμογή για διαχωρισμό οθόνης"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Ακύρωση"</b></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="blocked_by_policy" msgid="2071401072261365546">"Αυτή η ενέργεια δεν επιτρέπεται από την εφαρμογή ή τον οργανισμό σας."</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Γρήγορες ρυθμ."</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Γραμμή εργαλείων"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Η γραμμή εργαλείων εμφανίζεται"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Εμφάν. αριστ. γρ. εργ. και συν."</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Εμφάν. δεξιάς γρ. εργ. και συν."</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Η γραμμή εργαλείων είναι κρυφή"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Απόκρυψη εργαλείων και συννεφ."</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Γραμμή πλοήγησης"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Εμφάνιση Γραμμής εργαλείων"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Αλλαγή τρόπου πλοήγησης"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Διαχωριστικό Γραμμής εργαλείων"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Υπερχείλιση γραμμής εργαλείων"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Μετακίνηση επάνω/αριστερά"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Μετακίνηση κάτω/δεξιά"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Εμφάνιση # ακόμα εφαρμογής.}other{Εμφάνιση # ακόμα εφαρμογών.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Εμφάνιση # εφαρμογής υπολογιστή.}other{Εμφάνιση # εφαρμογών υπολογιστή.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ακόμη εφαρμογή}other{ακόμη εφαρμογές}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Υπολογιστής"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> και <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Συννεφάκι"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Υπερχείλιση"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> από <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> και <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ακόμα"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Μετακίνηση αριστερά"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Μετακίνηση δεξιά"</string>
+    <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 b84f646..6b81b05 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Clear all"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Save app pair"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choose another app to use split screen"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancel"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancel"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Quick Settings"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taskbar"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taskbar shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taskbar and bubbles left shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taskbar and bubbles right shown"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taskbar hidden"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taskbar and bubbles hidden"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigation bar"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Show # desktop app.}other{Show # desktop apps.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Move left"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Move right"</string>
+    <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 88cd0dd..da4effb 100644
--- a/quickstep/res/values-en-rCA/strings.xml
+++ b/quickstep/res/values-en-rCA/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Clear all"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Save app pair"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choose another app to use split screen"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancel"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancel"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organization"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Quick Settings"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taskbar"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taskbar shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taskbar &amp; bubbles left shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taskbar &amp; bubbles right shown"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taskbar hidden"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taskbar &amp; bubbles hidden"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigation bar"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar Divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar Overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Show # desktop app.}other{Show # desktop apps.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Move left"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Move right"</string>
+    <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 b84f646..6b81b05 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Clear all"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Save app pair"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choose another app to use split screen"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancel"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancel"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Quick Settings"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taskbar"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taskbar shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taskbar and bubbles left shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taskbar and bubbles right shown"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taskbar hidden"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taskbar and bubbles hidden"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigation bar"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Show # desktop app.}other{Show # desktop apps.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Move left"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Move right"</string>
+    <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 b84f646..6b81b05 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Clear all"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Save app pair"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choose another app to use split screen"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancel"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancel"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Quick Settings"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taskbar"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taskbar shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taskbar and bubbles left shown"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taskbar and bubbles right shown"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taskbar hidden"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taskbar and bubbles hidden"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigation bar"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Show # desktop app.}other{Show # desktop apps.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Move left"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Move right"</string>
+    <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Dismiss all"</string>
+    <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"expand <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"collapse <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml
index 76dab0d..8fcbe2b 100644
--- a/quickstep/res/values-en-rXC/strings.xml
+++ b/quickstep/res/values-en-rXC/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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‏‏‎‎‎‏‎‏‏‏‏‏‎‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‎‏‎‏‏‏‎‎‎‏‏‏‏‏‏‏‎Clear all‎‏‎‎‏‎"</string>
@@ -99,7 +100,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‎‏‎‏‎‏‏‎‏‎‏‏‎‏‎‏‏‎‎‏‎‏‏‎‏‎‏‎‏‏‎‏‎‏‎‏‎‏‏‎‎‎‎‏‏‏‎‏‎‏‏‎‏‎Save app pair‎‏‎‎‏‎"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‎‏‎‏‎‏‏‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‎‎‏‏‏‏‏‎‎‎‎‏‏‎‏‎‎Tap another app to use split screen‎‏‎‎‏‎"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‏‏‏‎‎‎‎‎‎‏‎‎‎‎‏‎‎‎‏‏‏‏‏‎‏‎‏‏‎‎‏‎‏‏‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‎‏‎‏‎‏‏‎‎‏‎‎Choose another app to use split screen‎‏‎‎‏‎"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‏‎‏‎‎‏‏‎‏‎‏‏‏‎‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‏‏‏‎‎‏‎‎‏‏‎"<b>"‎‏‎‎‏‏‏‎Cancel‎‏‎‎‏‏‎"</b>"‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‏‎‏‎‎‎‏‏‎‎‏‏‎‏‎‎‎‎‏‏‏‎‏‎‏‎‏‏‎‎‎‏‎‎‎‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‎‏‎‎Cancel‎‏‎‎‏‎"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‎‏‎‏‎‏‎‏‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‎‎‏‏‎‎‎‎‏‎‏‎‎‎‏‏‎‎‏‎‎Exit split screen selection‎‏‎‎‏‎"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‎‏‏‏‎‎‎‎‏‎‎‏‎‎‏‎‏‎‎‎‎‎‏‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‎Choose another app to use split screen‎‏‎‎‏‎"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‎‏‎‏‏‏‏‏‏‎‎‎‏‏‎‎‎‎‏‎‎‏‏‎‎‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‎‎‏‏‏‏‏‏‏‎‎‏‎‏‎‏‎‎This action isn\'t allowed by the app or your organization‎‏‎‎‏‎"</string>
@@ -130,18 +131,27 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‏‎‎‏‎‏‎‎‎‏‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‎‎‏‏‎‏‏‎‏‏‏‎‎‎‏‎‎‎‎‎‎‎‏‏‎‎‎‎‏‏‏‏‎Quick Settings‎‏‎‎‏‎"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‏‏‎‏‎‏‎‎‎‎‏‎‏‏‏‎‎‏‏‏‎‏‎‏‏‎‎‏‏‏‏‎‎‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‎‎Taskbar‎‏‎‎‏‎"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‏‎‏‏‎‏‎‏‎‎‎‎‎‏‏‏‎‎‎‏‏‎‎‎‏‏‎‎‏‏‏‏‎‏‎‏‏‏‎‎‏‎‏‏‎‏‏‏‏‎‎‎‏‎Taskbar shown‎‏‎‎‏‎"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‎‎‎‏‎‎‏‎‎‏‏‏‏‏‎‎‎‎‏‏‎‎‏‎‏‎‏‏‎‏‏‏‏‎‎Taskbar &amp; bubbles left shown‎‏‎‎‏‎"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‎‏‏‏‎‎‎‏‎‏‎‏‏‎‏‎‎‎‏‎‏‎‎‏‏‎‏‎‎‏‎‎‎‏‎Taskbar &amp; bubbles right shown‎‏‎‎‏‎"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‏‏‎‎‎‎‎‎‎‏‎‏‏‎‏‎‏‏‏‏‏‏‏‎‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎‏‎‎‏‎‏‎‎‎Taskbar hidden‎‏‎‎‏‎"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‏‎‏‎‎‎‏‏‎‏‎‏‎‎‎‏‏‏‎‏‏‎‎‏‏‏‎‎‏‎‏‎‎‏‎‎‏‏‎‎‎‎‏‎‎‏‏‏‏‏‎‏‏‏‎Taskbar &amp; bubbles hidden‎‏‎‎‏‎"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‏‎‏‏‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‎‏‏‎‏‎‎‎‏‏‎‏‎‎‏‏‎Navigation bar‎‏‎‎‏‎"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‎‏‎‏‎‏‎‎‎‎‏‎‎‏‏‎‏‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‎Always show Taskbar‎‏‎‎‏‎"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‎‎‎‏‏‏‎‏‎‏‎‏‎‏‏‎‏‎‎‏‎‎‏‏‎‏‎‎‏‏‏‎‎‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‎‎‎Change navigation mode‎‏‎‎‏‎"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‏‎‏‏‎‏‏‎‎‏‎‎‎‏‏‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‏‏‎‎‎‎‏‏‏‏‎‏‏‏‏‎‏‎‎‎‎‎‎Taskbar Divider‎‏‎‎‏‎"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎Taskbar Overflow‎‏‎‎‏‎"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‎‏‎‏‏‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‎‏‏‎‏‎‏‏‎‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‎Move to top/left‎‏‎‎‏‎"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‏‏‏‎‎‏‏‎‏‏‏‏‎‎‎‎‏‎‎Move to bottom/right‎‏‎‎‏‎"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‏‎Show # more app.‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‏‎Show # more apps.‎‏‎‎‏‎}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‏‏‏‏‏‎‎‏‏‏‏‎‎‏‎‏‎‎‎‏‎‏‎‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‏‎‏‏‏‏‎‎‎‎Show # desktop app.‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‏‏‏‏‏‎‎‏‏‏‏‎‎‏‎‏‎‎‎‏‎‏‎‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‏‎‏‏‏‏‎‎‎‎Show # desktop apps.‎‏‎‎‏‎}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‎‎‎‎‎more app‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‎‎‎‎‎more apps‎‏‎‎‏‎}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‏‎‎‏‎‎‎‏‎‏‎‎‏‏‎‎‎‏‎‎‏‎‏‎Desktop‎‏‎‎‏‎"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‎‏‏‎‎‎‎‏‏‎‎‏‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‎‏‎‎‎‏‏‏‏‏‏‎‎‎‎‎‏‎‎‎‎‎‏‎‎‏‏‎<xliff:g id="APP_NAME_1">%1$s</xliff:g>‎‏‎‎‏‏‏‎ and ‎‏‎‎‏‏‎<xliff:g id="APP_NAME_2">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‎‎‎‏‎‏‎‏‏‎‏‏‎‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‏‎‎‎‏‏‏‎‎‏‎‏‏‎‏‏‏‏‎‏‎‎‏‎Bubble‎‏‎‎‏‎"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‎‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‎‎Overflow‎‏‎‎‏‎"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‏‏‎‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>‎‏‎‎‏‏‏‎ from ‎‏‎‎‏‏‎<xliff:g id="APP_NAME">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‎‎‏‎‎‎‏‎‏‏‏‎‏‎‎‏‎‎‏‎‏‎‎‎‏‎‏‏‎‎‏‎‎‏‏‎‎‎‎‎‎‎‏‎‎‏‏‎<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>‎‏‎‎‏‏‏‎ and ‎‏‎‎‏‏‎<xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>‎‏‎‎‏‏‏‎ more‎‏‎‎‏‎"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‏‏‎‎‏‎‎‏‎‎‎‏‏‎‎‎‏‏‏‏‎‎‎‏‎‎‎‏‎‏‎‎‎‎‏‏‎‏‎‎‎‎‏‎‎‎‎‎‏‎‎‎‏‏‎‎Move left‎‏‎‎‏‎"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‏‎‏‏‎‏‎‎‎‎‏‎‏‏‏‏‎‎‏‎‏‎‎‎‎‏‎‎‏‎‎‏‏‏‎‎‏‏‎‎‏‏‎Move right‎‏‎‎‏‎"</string>
+    <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>
 </resources>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index 58d540f..57333f4 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Cerrar todo"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Guardar vinculación"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Presiona otra app para usar la pantalla dividida"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Elige otra app para usar la pantalla dividida"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancelar"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancelar"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Salir de la selección de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Elige otra app para usar la pantalla dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"La app o tu organización no permiten realizar esta acción"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Config. rápida"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barra de tareas"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Barra de tareas visible"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barra tareas y burb. a la izq."</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barra tareas y burb. a la der."</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Barra de tareas oculta"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barra tareas y burb. ocultas"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barra de navegación"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barra de tareas visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambiar el modo de navegación"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor de la Barra de tareas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tareas ampliada"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover a la parte superior o izquierda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover a la parte inferior o derecha"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar # app más.}other{Mostrar # apps más.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Mostrar # app para computadoras.}other{Mostrar # apps para computadoras.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app más}other{apps más}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Escritorio"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> y <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Burbuja"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ampliada"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> y <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> más"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Mover hacia la izquierda"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Mover hacia la derecha"</string>
+    <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 2a956f0..8355a88 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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">"Ordenador"</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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Borrar todo"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Guardar apps emparejadas"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toca otra aplicación para usar la pantalla dividida"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Elige otra app para usar la pantalla dividida."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancelar"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancelar"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Salir de la selección de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Elige otra app para usar la pantalla dividida."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"No puedes hacerlo porque la aplicación o tu organización no lo permiten"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Ajustes rápidos"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barra de tareas"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Barra de tareas visible"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Tareas y burbujas a izquierda"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Tareas y burbujas a derecha"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Barra de tareas oculta"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Tareas y burbujas ocultas"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barra de navegación"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barra de tareas visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambiar el modo de navegación"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor de Barra de Tareas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tareas ampliada"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover arriba/a la izquierda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover abajo/a la derecha"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar # aplicación más.}other{Mostrar # aplicaciones más.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Mostrar # aplicación para ordenadores.}other{Mostrar # aplicaciones para ordenadores.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicación más}other{aplicaciones más}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Ordenador"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> y <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Burbuja"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Menú adicional"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> y <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> más"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Mover hacia la izquierda"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Mover hacia la derecha"</string>
+    <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 0ecc0c0..6192e81 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Sule kõik"</string>
@@ -72,7 +74,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Väga hea!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Pühkige kindlasti ekraani alumisest servast üles."</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Hoidke sõrme aknal pisut kauem, enne kui vabastate"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Pühkige kindlasti otse üles, seejärel peatuge"</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Pühkige kindlasti otse üles, seejärel peatuge."</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Õppisite liigutusi kasutama. Liigutuste väljalülitamiseks avage Seaded."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Tegite rakenduste vahel vahetamise liigutuse"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Pühkige rakenduste vahetamiseks"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Salvesta rakendusepaar"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Jagatud ekraanikuva kasutamiseks puudutage muud rakendust"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Valige jagatud ekraanikuva jaoks muu rakendus."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Tühista"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Tühista"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Jagatud ekraanikuva valikust väljumine"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Valige jagatud ekraanikuva jaoks muu rakendus."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Rakendus või teie organisatsioon on selle toimingu keelanud"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Kiirseaded"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Tegumiriba"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Tegumiriba on kuvatud"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Tegumiriba ja mullid kuvatakse"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Tegumiriba ja mullid kuvatakse paremal"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Tegumiriba on peidetud"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Tegumiriba ja mullid on peidetud"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigeerimisriba"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Kuva tegumiriba alati"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigeerimisrežiimi muutmine"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Tegumiriba jagaja"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tegumiriba ületäide"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Teisalda üles/vasakule"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Teisalda alla/paremale"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Kuva veel # rakendus.}other{Kuva veel # rakendust.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Kuva # töölauarakendus.}other{Kuva # töölauarakendust.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{rakendus veel}other{rakendust veel}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Lauaarvuti"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ja <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Mull"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ületäide"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ja veel <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> mulli"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Liigu vasakule"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Liigu paremale"</string>
+    <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 e83ee28..de19f15 100644
--- a/quickstep/res/values-eu/strings.xml
+++ b/quickstep/res/values-eu/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Garbitu guztiak"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Gorde aplikazio parea"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Sakatu beste aplikazio bat pantaila zatitzeko"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Pantaila zatitua erabiltzeko, aukeratu beste aplikazio bat"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Utzi"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Utzi"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Irten pantaila zatituaren hautapenetik"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pantaila zatitzeko, aukeratu beste aplikazio bat"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikazioak edo erakundeak ez du eman ekintza hori gauzatzeko baimena"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Ezarpen bizkorrak"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Zereginen barra"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Zereginen barra ikusgai dago"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Zereginen barra eta burbuilak ezkerrean ikusgai"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Zereginen barra eta burbuilak eskuinean ikusgai"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Zereginen barra itxita dago"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Zereginen barra eta burbuilak ezkutatuta"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Nabigazio-barra"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Erakutsi beti zereginen barra"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Aldatu nabigazio modua"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Zereginen barraren zatitzailea"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Zereginen barraren luzapena"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Eraman gora, ezkerretara"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Eraman behera, eskuinetara"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Erakutsi beste # aplikazio.}other{Erakutsi beste # aplikazio.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Erakutsi ordenagailuetarako # aplikazio.}other{Erakutsi ordenagailuetarako # aplikazio.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikazio gehiago}other{aplikazio gehiago}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Ordenagailua"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> eta <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Burbuila"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Luzapena"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> eta beste <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Eraman ezkerrera"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Eraman eskuinera"</string>
+    <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 bafc2d5..bc14f0b 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"پاک کردن همه"</string>
@@ -99,11 +101,11 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ذخیره جفت برنامه"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"زدن روی برنامه‌ای دیگر برای استفاده از صفحه دونیمه"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"انتخاب برنامه‌ای دیگر برای استفاده از صفحه دونیمه"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"لغو کردن"</b></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="blocked_by_policy" msgid="2071401072261365546">"برنامه یا سازمان شما اجازه نمی‌دهد این کنش انجام شود."</string>
-    <string name="split_widgets_not_supported" msgid="1355743038053053866">"درحال‌حاضر از ابزارک‌ها پشتیبانی نمی‌شود، لطفاً برنامه دیگری را انتخاب کنید"</string>
+    <string name="split_widgets_not_supported" msgid="1355743038053053866">"درحال‌حاضر از ابزاره‌ها پشتیبانی نمی‌شود، لطفاً برنامه دیگری را انتخاب کنید"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"آموزش گام‌به‌گام پیمایش رد شود؟"</string>
     <string name="skip_tutorial_dialog_subtitle" msgid="544063326241955662">"می‌توانید آن را بعداً در برنامه <xliff:g id="NAME">%1$s</xliff:g> پیدا کنید"</string>
     <string name="gesture_tutorial_action_button_label_cancel" msgid="3809842569351264108">"لغو"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"تنظیمات فوری"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"نوار وظیفه"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"نوار وظیفه نمایان است"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"نمایش نوار وظیفه و حبابک‌ها در چپ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"نمایش نوار وظیفه و حبابک‌ها در راست"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"نوار وظیفه پنهان است"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"نوار وظیفه و حبابک‌ها پنهان هستند"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"نوار پیمایش"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"نوار وظیفه همیشه نشان داده شود"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"تغییر حالت پیمایش"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"جداکننده نوار وظیفه"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"سرریز نوار وظیفه"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"انتقال به بالا/ چپ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"انتقال به پایین/ راست"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{نمایش # برنامه دیگر.}one{نمایش # برنامه دیگر.}other{نمایش # برنامه دیگر.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{نمایش # برنامه رایانه.}one{نمایش # برنامه رایانه.}other{نمایش # برنامه رایانه.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{برنامه دیگر}one{برنامه دیگر}other{برنامه دیگر}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"رایانه"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> و <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"حبابک"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"سرریز"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> و <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> حبابک دیگر"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"انتقال به چپ"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"انتقال به راست"</string>
+    <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 5ac124a..10e4699 100644
--- a/quickstep/res/values-fi/strings.xml
+++ b/quickstep/res/values-fi/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Poista kaikki"</string>
@@ -72,7 +74,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Hienoa!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Pyyhkäise ylös näytön alareunasta"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Kokeile pitää ikkunaa painettuna pidempään ennen kuin päästät irti"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Muista pyyhkäistä suoraan ylöspäin ja pysähdy sitten"</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Pyyhkäise suoraan ylöspäin ja pysähdy sitten"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Olet oppinut käyttämään eleitä. Jos haluat laittaa eleet pois päältä, avaa Asetukset."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Olet oppinut sovellusten vaihtamiseleen"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Vaihda sovellusta pyyhkäisemällä"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Tallenna pari"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Avaa jaettu näyttö napauttamalla toista sovellusta"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Käytä jaettua näyttöä valitsemalla toinen sovellus"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Peruuta"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Peru"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Poistu jaetun näytön valinnasta"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Käytä jaettua näyttöä valitsemalla toinen sovellus"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Sovellus tai organisaatio ei salli tätä toimintoa"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Pika-asetukset"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Tehtäväpalkki"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Tehtäväpalkki näkyvissä"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Vas. palkki ja kuplat näkyvissä"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Oik. palkki ja kuplat näkyvissä"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Tehtäväpalkki piilotettu"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Palkki ja kuplat piilotettu"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigointipalkki"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Näytä tehtäväpalkki aina"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Vaihda navigointitilaa"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Tehtäväpalkin jakaja"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tehtäväpalkin ylivuotu"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Siirrä ylös tai vasemmalle"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Siirrä alas tai oikealle"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Näytä # muu sovellus.}other{Näytä # muuta sovellusta.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Näytä # työpöytäsovellus.}other{Näytä # työpöytäsovellusta.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{muu sovellus}other{muuta sovellusta}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Tietokone"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ja <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Kupla"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ylivuoto"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ja <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> muuta"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Siirrä vasemmalle"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Siirrä oikealle"</string>
+    <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 9510494..746bf50 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Tout effacer"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Enr. paire d\'applis"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toucher une autre appli pour partager l\'écran"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choisir une autre appli pour utiliser l\'Écran divisé"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Annuler"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Annuler"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Quitter la sélection d\'écran divisé"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choisir une autre appli pour utiliser l\'écran partagé"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"L\'appli ou votre organisation n\'autorise pas cette action"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Paramètres rapides"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barre des tâches"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Barre des tâches affichée"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barre des tâches/bulles à gauche"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barre des tâches/bulles à droite"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Barre des tâches masquée"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barre des tâches/bulles masquées"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barre de navigation"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Tjrs afficher barre des tâches"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Changer de mode de navigation"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Séparateur de la barre des tâches"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barre des tâches à développer"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Déplacer vers le coin supérieur gauche de l\'écran"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer vers le coin inférieur droit de l\'écran"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre appli.}one{Afficher # autre appli.}other{Afficher # autres applis.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Afficher # appli de bureau.}one{Afficher # appli de bureau.}other{Afficher # applis de bureau.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{autre appli}one{autre appli}other{autres applis}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Ordinateur de bureau"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> et <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bulle"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Bulle à développer"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> autres"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Déplacer vers la gauche"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Déplacer vers la droite"</string>
+    <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 60f8944..6d2fba2 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Tout effacer"</string>
@@ -88,7 +90,7 @@
     <string name="gesture_tutorial_nice" msgid="2936275692616928280">"Bravo !"</string>
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"Tutoriel <xliff:g id="CURRENT">%1$d</xliff:g> sur <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"Tout est prêt !"</string>
-    <string name="allset_hint" msgid="459504134589971527">"Balayez l\'écran vers le haut pour revenir à l\'accueil"</string>
+    <string name="allset_hint" msgid="459504134589971527">"Balayez vers le haut pour revenir à l\'accueil"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Appuyez sur le bouton d\'accueil pour accéder à votre écran d\'accueil"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Vous pouvez maintenant utiliser votre <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"appareil"</string>
@@ -96,10 +98,10 @@
     <string name="action_share" msgid="2648470652637092375">"Partager"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Capture d\'écran"</string>
     <string name="action_split" msgid="2098009717623550676">"Partager"</string>
-    <string name="action_save_app_pair" msgid="5974823919237645229">"Enregistrer la paire d\'applis"</string>
+    <string name="action_save_app_pair" msgid="5974823919237645229">"Enregistrer une paire d\'applis"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Appuyez sur autre appli pour l\'écran partagé"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Sélectionnez une autre appli pour utiliser l\'écran partagé."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Annuler"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Annuler"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Quitter la sélection de l\'écran partagé"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Sélect. autre appli pour utiliser l\'écran partagé"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Cette action n\'est pas autorisée par l\'application ou par votre organisation"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Réglages rapides"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barre des tâches"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Barre des tâches affichée"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barre des tâches et bulles affichées à gauche"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barre des tâches et bulles affichées à droite"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Barre des tâches masquée"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barre des tâches et bulles masquées"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barre de navigation"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barre des tâches tjs visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Modifier le mode de navigation"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Séparateur de barre des tâches"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Développement de la barre des tâches"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Déplacer en haut ou à gauche"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer en bas ou à droite"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre appli}one{Afficher # autre appli}other{Afficher # autre applis}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Afficher # application de bureau.}one{Afficher # application de bureau.}other{Afficher # applications de bureau.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{autre application}one{autre application}other{autres applications}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Ordinateur"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> et <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bulle"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Dépassement"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> autre(s)"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Déplacer vers la gauche"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Déplacer vers la droite"</string>
+    <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 bf081d4..d9a78ee 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Borrar todo"</string>
@@ -90,8 +92,8 @@
     <string name="allset_title" msgid="5021126669778966707">"Todo listo"</string>
     <string name="allset_hint" msgid="459504134589971527">"Pasa o dedo cara arriba para ir á pantalla de inicio"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Toca o botón de inicio para ir á pantalla de inicio"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"Xa podes comezar a utilizar o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>)"</string>
-    <string name="default_device_name" msgid="6660656727127422487">"dispositivo"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> dispoñible para comezar a utilizar"</string>
+    <string name="default_device_name" msgid="6660656727127422487">"Dispositivo"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Configuración da navegación do sistema"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Compartir"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Facer captura"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Gardar parella apps"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Para usar a pantalla dividida, toca outra app"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Escolle outra aplicación para usar a pantalla dividida."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancelar"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancelar"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Saír da selección de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolle outra app para usar a pantalla dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"A aplicación ou a túa organización non permite realizar esta acción"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Configuración rápida"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barra de tarefas"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Estase mostrando a barra de tarefas"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barra e burbullas á esquerda"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barra e burbullas á dereita"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Non se está mostrando a barra de tarefas"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barra e burbullas ocultas"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barra de navegación"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ver sempre a barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambiar modo de navegación"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Menú adicional da barra de tarefas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover á parte superior ou á esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover á parte inferior ou á dereita"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar # aplicación máis.}other{Mostrar # aplicacións máis.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Mostrar # aplicación para ordenadores.}other{Mostrar # aplicacións para ordenadores.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicación máis}other{aplicacións máis}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Escritorio"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Burbulla"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Menú adicional"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> máis"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Mover cara á esquerda"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Mover cara á dereita"</string>
+    <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 b20f771..1bdcaa1 100644
--- a/quickstep/res/values-gu/strings.xml
+++ b/quickstep/res/values-gu/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"બધું સાફ કરો"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ઍપની જોડી સાચવો"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"વિભાજિત સ્ક્રીન વાપરવા, કોઈ અન્ય ઍપ પર ટૅપ કરો"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"વિભાજિત સ્ક્રીનની સુવિધાનો ઉપયોગ કરવા કોઈ અન્ય ઍપ પસંદ કરો"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"રદ કરો"</b></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="blocked_by_policy" msgid="2071401072261365546">"ઍપ કે તમારી સંસ્થા દ્વારા આ ક્રિયા કરવાની મંજૂરી નથી"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ઝડપી સેટિંગ"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ટાસ્કબાર"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ટાસ્કબાર બતાવવામાં આવ્યો"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ટાસ્કબાર અને બબલ ડાબે બતાવ્યા"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ટાસ્કબાર અને બબલ જમણે બતાવ્યા"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ટાસ્કબાર છુપાવવામાં આવ્યો"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ટાસ્કબાર અને બબલ છુપાવેલા છે"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"નૅવિગેશન બાર"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"હંમેશાં ટાસ્કબાર બતાવો"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"નૅવિગેશન મોડ બદલો"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ટાસ્કબાર વિભાજક"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ટાસ્કબાર ઓવરફ્લો"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"સૌથી ઉપર ડાબી બાજુએ ખસેડો"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"સૌથી નીચે જમણી બાજુએ ખસેડો"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{વધુ # ઍપ બતાવો.}one{વધુ # ઍપ બતાવો.}other{વધુ # ઍપ બતાવો.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ડેસ્કટૉપ ઍપ બતાવો.}one{# ડેસ્કટૉપ ઍપ બતાવો.}other{# ડેસ્કટૉપ ઍપ બતાવો.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{વધુ ઍપ}one{વધુ ઍપ}other{વધુ ઍપ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ડેસ્કટૉપ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> અને <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"બબલ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ઓવરફ્લો"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>થી <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> અને વધુ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ડાબે ખસેડો"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"જમણે ખસેડો"</string>
+    <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 a645186..e97aa78 100644
--- a/quickstep/res/values-hi/strings.xml
+++ b/quickstep/res/values-hi/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"सभी हटाएं"</string>
@@ -92,14 +94,14 @@
     <string name="allset_button_hint" msgid="2395219947744706291">"होम स्क्रीन पर जाने के लिए, होम बटन पर टैप करें"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"अब <xliff:g id="DEVICE">%1$s</xliff:g> इस्तेमाल के लिए तैयार है"</string>
     <string name="default_device_name" msgid="6660656727127422487">"डिवाइस"</string>
-    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"सिस्टम नेविगेशन सेटिंग"</annotation></string>
+    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"सिस्टम नेविगेशन से जुड़ी सेटिंग"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"शेयर करें"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"स्क्रीनशॉट लें"</string>
     <string name="action_split" msgid="2098009717623550676">"स्प्लिट स्क्रीन मोड"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"ऐप पेयर सेव करें"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिट स्क्रीन के लिए दूसरे ऐप्लिकेशन पर टैप करें"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"स्प्लिट स्क्रीन इस्तेमाल करने के लिए, दूसरा ऐप्लिकेशन चुनें"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"अभी नहीं"</b></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="blocked_by_policy" msgid="2071401072261365546">"ऐप्लिकेशन या आपका संगठन इस कार्रवाई की अनुमति नहीं देता"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"फटाफट सेटिंग"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"टास्कबार"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"टास्कबार दिखाया गया"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"टास्कबार और बबल बाईं ओर हैं"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"टास्कबार और बबल दाईं ओर हैं"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"टास्कबार छिपाया गया"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"टास्कबार और बबल छिपाए गए"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"नेविगेशन बार"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"टास्कबार हमेशा दिखाएं"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"नेविगेशन का मोड बदलें"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"टास्कबार डिवाइडर"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"टास्कबार ओवरफ़्लो आइकॉन"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ऊपर/बाईं तरफ़ ले जाएं"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"नीचे/दाईं तरफ़ ले जाएं"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# और ऐप्लिकेशन दिखाएं.}one{# और ऐप्लिकेशन दिखाएं.}other{# और ऐप्लिकेशन दिखाएं.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# डेस्कटॉप ऐप्लिकेशन दिखाएं.}one{# डेस्कटॉप ऐप्लिकेशन दिखाएं.}other{# डेस्कटॉप ऐप्लिकेशन दिखाएं.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ज़्यादा ऐप्लिकेशन}one{ज़्यादा ऐप्लिकेशन}other{ज़्यादा ऐप्लिकेशन}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"डेस्कटॉप"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> और <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"बबल"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ओवरफ़्लो"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> की <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> वाली सूचना"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> और <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> अन्य"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"बाईं ओर ले जाएं"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"दाईं ओर ले जाएं"</string>
+    <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 c96381d..c350bc5 100644
--- a/quickstep/res/values-hr/strings.xml
+++ b/quickstep/res/values-hr/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Izbriši sve"</string>
@@ -47,7 +49,7 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Predviđena aplikacija: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Zakrenite uređaj"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Zakrenite uređaj da biste dovršili vodič o navigaciji pokretima"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Pazite da prijeđete prstom od krajnjeg desnog ili krajnjeg lijevog ruba"</string>
+    <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>
@@ -70,9 +72,9 @@
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Otvaranje početnog zaslona"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Prijeđite prstom od dna zaslona prema gore"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Sjajno!"</string>
-    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Pazite da prijeđete prstom prema gore od donjeg ruba zaslona"</string>
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Prijeđite prstom prema gore od donjeg ruba zaslona"</string>
     <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">"Pazite da prijeđete prstom ravno prema gore, a zatim zastanete"</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_intro_title" msgid="2902054412868489378">"Povlačenje prstom za promjenu aplikacije"</string>
@@ -90,8 +92,8 @@
     <string name="allset_title" msgid="5021126669778966707">"Sve je spremno!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Prijeđite prstom prema gore da biste otvorili početni zaslon"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Dodirnite gumb početnog zaslona da biste prešli na početni zaslon"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> je spreman za početak upotrebe"</string>
-    <string name="default_device_name" msgid="6660656727127422487">"uređaj"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"Možete početi upotrebljavati <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
+    <string name="default_device_name" msgid="6660656727127422487">"Uređaj"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Postavke navigacije sustavom"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Podijeli"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Snimka zaslona"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Spremi par apl."</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu aplikaciju za podijeljeni zaslon"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Odaberite drugu aplikaciju za upotrebu podijeljenog zaslona"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Odustani"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Odustani"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Zatvori odabir podijeljenog zaslona"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu aplikaciju za upotrebu podijeljenog zaslona"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ili vaša organizacija ne dopuštaju ovu radnju"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Brze postavke"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Traka sa zadacima"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Traka sa zadacima prikazana"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Traka sa zadacima/oblačići prikazani lijevo"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Traka sa zadacima/oblačići prikazani desno"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Traka sa zadacima skrivena"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Traka sa zadacima/oblačići skriveni"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigacijska traka"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvijek prikaži traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promijeni način navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdjelnik trake sa zadacima"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Dodatni izbornik trake sa zadacima"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premjesti gore/lijevo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje/desno"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Prikaži još # aplikaciju}one{Prikaži još # aplikaciju}few{Prikaži još # aplikacije}other{Prikaži još # aplikacija}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Prikaži # računalnu aplikaciju.}one{Prikaži # računalnu aplikaciju.}few{Prikaži # računalne aplikacije.}other{Prikaži # računalnih aplikacija.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Računalo"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Oblačić"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Dodatni izbornik"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Pomakni ulijevo"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Pomakni udesno"</string>
+    <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 03235aa..ea29620 100644
--- a/quickstep/res/values-hu/strings.xml
+++ b/quickstep/res/values-hu/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Összes törlése"</string>
@@ -59,7 +61,7 @@
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Ha vissza szeretne térni a legutóbbi képernyőre, csúsztasson gyorsan két ujjal a képernyő bal vagy jobb széléről a közepe felé."</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Vissza"</string>
     <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Csúsztasson bal vagy jobb szélről a képernyő közepe felé."</string>
-    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Csúsztasson felfelé a képernyő aljától."</string>
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Csúsztasson felfelé a képernyő alsó szélétől."</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Ne álljon meg, mielőtt elengedi a képernyőt."</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Csúsztasson egyenesen felfelé."</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Teljesítette a kezdőképernyőre lépés kézmozdulatát. Most megtanulhatja, hogyan léphet vissza."</string>
@@ -68,9 +70,9 @@
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Csúsztassa ujját felfelé a képernyő aljától. Ez a mozdulat mindig a kezdőképernyőre visz."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Csúsztasson felfelé két ujjal a képernyő aljáról. Ez a kézmozdulat mindig a kezdőképernyőre viszi."</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Ugrás a kezdőképernyőre"</string>
-    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Húzza ujját felfelé a képernyő aljától."</string>
+    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Csúsztasson felfelé a képernyő aljától."</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Kiváló!"</string>
-    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Csúsztasson felfelé a képernyő aljától."</string>
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Csúsztasson felfelé a képernyő alsó szélétől."</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Próbálja tovább lenyomva tartani az ablakot, mielőtt elengedi a képernyőt."</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Csúsztasson egyenesen felfelé, majd várjon egy kicsit"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Eddig megismerhette a kézmozdulatok használatát. A kézmozdulatokat a Beállításokban kapcsolhatja ki."</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"App-pár mentése"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Koppintson másik appra az osztott képernyőhöz"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Válasszon másik appot a képernyő felosztásához"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Mégse"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Mégse"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Kilépés az osztott képernyő elemeinek kiválasztásából"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Válasszon másik appot a képernyő felosztásához"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Az alkalmazás vagy az Ön szervezete nem engedélyezi ezt a műveletet"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Gyorsbeállítások"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Tálca"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Feladatsáv megjelenítve"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Feladatsáv és buborék balra"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Feladatsáv és buborék jobbra"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Feladatsáv elrejtve"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Rejtett feladatsáv és buborék"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigációs sáv"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Mindig megjelenő Feladatsáv"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigációs mód módosítása"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Feladatsáv-elválasztó"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Feladatsáv túlcsordulása"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mozgatás felülre vagy a bal oldalra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mozgatás alulra vagy a jobb oldalra"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# további alkalmazás megjelenítése.}other{# további alkalmazás megjelenítése.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# asztali alkalmazás megjelenítése.}other{# asztali alkalmazás megjelenítése.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{további alkalmazás}other{további alkalmazás}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Asztali"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> és <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Buborék"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Túlcsordulás"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, forrás: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> és <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> további"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Mozgatás balra"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Mozgatás jobbra"</string>
+    <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 3cb7990..14d715d 100644
--- a/quickstep/res/values-hy/strings.xml
+++ b/quickstep/res/values-hy/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Փակել բոլորը"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Պահել հավելվ․ զույգը"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Հպեք այլ հավելվածի՝ տրոհված էկրանից օգտվելու համար"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Ընտրեք այլ հավելված՝ տրոհված էկրանից օգտվելու համար"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Չեղարկել"</b></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="blocked_by_policy" msgid="2071401072261365546">"Այս գործողությունն արգելված է հավելվածի կամ ձեր կազմակերպության կողմից"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Արագ կարգավորումներ"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Խնդրագոտի"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Խնդրագոտին ցուցադրվում է"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Հավելվածների վահանակն ու ամպիկները տեսանելի են ձախ կողմում"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Հավելվածների վահանակն ու ամպիկները տեսանելի են աջ կողմում"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Խնդրագոտին թաքցված է"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Հավելվածների վահանակն ու ամպիկները թաքցված են"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Նավիգացիայի գոտի"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Միշտ ցույց տալ վահանակը"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Փոխել նավիգացիայի ռեժիմը"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Հավելվածների վահանակի բաժանիչ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Հավելվածների վահանակի լրացուցիչ ընտրացանկ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Տեղափոխել վերևի ձախ անկյուն"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Տեղափոխել ներքևի աջ անկյուն"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Ցուցադրել ևս # հավելված։}one{Ցուցադրել ևս # հավելված։}other{Ցուցադրել ևս # հավելված։}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Ցույց տալ # համակարգչային հավելված։}one{Ցույց տալ # համակարգչային հավելված։}other{Ցույց տալ # համակարգչային հավելված։}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{լրացուցիչ հավելված}one{լրացուցիչ հավելված}other{լրացուցիչ հավելված}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Համակարգիչ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> և <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Ամպիկ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Լրացուցիչ ընտրացանկ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>՝ <xliff:g id="APP_NAME">%2$s</xliff:g> հավելվածից"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ու ևս <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ամպիկ"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Տեղափոխել ձախ"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Տեղափոխել աջ"</string>
+    <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 015b09e..e1400a9 100644
--- a/quickstep/res/values-in/strings.xml
+++ b/quickstep/res/values-in/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Hapus semua"</string>
@@ -79,7 +81,7 @@
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Untuk beralih dari satu aplikasi ke aplikasi lain, geser ke atas dari bagian bawah layar, tahan, lalu lepaskan."</string>
     <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 lepaskan"</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="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Semua siap"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Selesai"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Simpan pasangan apl"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ketuk aplikasi lain untuk memakai layar terpisah"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Pilih aplikasi lain untuk dibuka di layar terpisah"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Batal"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Batal"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Keluar dari pemilihan layar terpisah"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pilih aplikasi lain untuk dibuka di layar terpisah"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Tindakan ini tidak diizinkan oleh aplikasi atau organisasi Anda"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Setelan Cepat"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taskbar"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taskbar ditampilkan"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taskbar &amp; balon kiri ditampilkan"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taskbar &amp; bubbles kanan ditampilkan"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taskbar disembunyikan"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taskbar &amp; balon disembunyikan"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Menu navigasi"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Selalu tampilkan Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Ubah mode navigasi"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Pemisah Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tambahan Taskbar"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Pindahkan ke atas/kiri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pindahkan ke bawah/kanan"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Tampilkan # aplikasi lainnya.}other{Tampilkan # aplikasi lainnya.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Tampilkan # aplikasi desktop.}other{Tampilkan # aplikasi desktop.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikasi lainnya}other{aplikasi lainnya}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> dan <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Balon"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Tambahan"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> dari <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> dan <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> lainnya"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Pindahkan ke kiri"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Pindahkan ke kanan"</string>
+    <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 883fe82..bf2a211 100644
--- a/quickstep/res/values-is/strings.xml
+++ b/quickstep/res/values-is/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Hreinsa allt"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Vista forritapar"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ýttu á annað forrit til að nota skjáskiptingu"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Veldu annað forrit til að nota skjáskiptingu"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Hætta við"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Hætta við"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Loka skjáskiptingu"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Veldu annað forrit til að nota skjáskiptingu"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Forritið eða fyrirtækið leyfir ekki þessa aðgerð"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Flýtistillingar"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Verkstika"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Forritastika sýnd"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Verkstika og blöðrur sýndar til vinstri"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Verkstika og blöðrur sýndar til hægri"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Forritastika falin"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Verkstika og blöðrur eru faldar"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Yfirlitsstika"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Alltaf sýna forritastiku"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Breyta leiðsagnarstillingu"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Skipting forritastiku"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Yfirflæði á forritastiku"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Færa efst/til vinstri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Færa neðst/til hægri"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Sýna # forrit í viðbót.}one{Sýna # forrit í viðbót.}other{Sýna # forrit í viðbót.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Sýna # skjáborðsforrit.}one{Sýna # skjáborðsforrit.}other{Sýna # skjáborðsforrit.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{forrit til viðbótar}one{forrit til viðbótar}other{forrit til viðbótar}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Skjáborð"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> og <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Blaðra"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Yfirflæði"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> frá <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> í viðbót"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Færa til vinstri"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Færa til hægri"</string>
+    <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 59b195a..af77be4 100644
--- a/quickstep/res/values-it/strings.xml
+++ b/quickstep/res/values-it/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Cancella tutto"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Salva coppia di app"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tocca un\'altra app per usare lo schermo diviso"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Scegli un\'altra app per usare lo schermo diviso"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Annulla"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Annulla"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Esci dalla selezione dello schermo diviso"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Scegli un\'altra app per usare lo schermo diviso"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Questa azione non è consentita dall\'app o dall\'organizzazione"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Impostazioni rapide"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barra delle applicazioni"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Barra delle app visualizzata"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barra app e bolle most. sinis."</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barra app e bolle most. destr."</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Barra delle app nascosta"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barra app e bolle nascoste"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barra di navigazione"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Mostra sempre barra app"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambia modalità di navigazione"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisore barra delle app"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Overflow barra delle app"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sposta in alto/a sinistra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sposta in basso/a destra"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostra # altra app.}other{Mostra altre # app.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Mostra # app desktop.}other{Mostra # app desktop.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{altra app}other{altre app}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Fumetto"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Extra"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> da <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e altri <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Sposta a sinistra"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Sposta a destra"</string>
+    <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 9f0ed14..ad26421 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ניקוי הכול"</string>
@@ -72,8 +74,8 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"מעולה!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"חשוב להחליק למעלה מהקצה התחתון של המסך"</string>
     <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_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_intro_title" msgid="2902054412868489378">"יש להחליק כדי לעבור בין אפליקציות"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"כדי לעבור בין אפלקציות, יש להחליק למעלה מתחתית המסך, להחזיק ולאחר מכן לשחרר."</string>
@@ -88,18 +90,18 @@
     <string name="gesture_tutorial_nice" msgid="2936275692616928280">"יפה!"</string>
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"מדריך <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"הכול מוכן!"</string>
-    <string name="allset_hint" msgid="459504134589971527">"כדי לחזור לדף הבית, מחליקים כלפי מעלה"</string>
-    <string name="allset_button_hint" msgid="2395219947744706291">"כדי לעבור אל מסך הבית יש להקיש על הלחצן הראשי"</string>
+    <string name="allset_hint" msgid="459504134589971527">"כדי לחזור לדף הבית, צריך להחליק למעלה"</string>
+    <string name="allset_button_hint" msgid="2395219947744706291">"כדי לעבור אל מסך הבית צריך להקיש על הלחצן הראשי"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"הכול מוכן ואפשר להתחיל להשתמש ב<xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"מכשיר"</string>
-    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"הגדרות הניווט של המערכת"</annotation></string>
+    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"הגדרות הניווט במערכת"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"שיתוף"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"צילום מסך"</string>
     <string name="action_split" msgid="2098009717623550676">"פיצול"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"שמירת צמד אפליקציות"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"צריך להקיש על אפליקציה אחרת כדי להשתמש במסך מפוצל"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"כדי להשתמש במסך מפוצל צריך לבחור אפליקציה אחרת"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ביטול"</b></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="blocked_by_policy" msgid="2071401072261365546">"האפליקציה או הארגון שלך אינם מתירים את הפעולה הזאת"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"הגדרות מהירות"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"סרגל האפליקציות"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"סרגל האפליקציות מוצג"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"סרגל האפליקציות והבועות מוצגים משמאל"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"סרגל האפליקציות והבועות מוצגים מימין"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"סרגל האפליקציות מוסתר"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"סרגל האפליקציות והבועות הוסתרו"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"סרגל הניווט"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"סרגל האפליקציות מוצג תמיד"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"שינוי מצב הניווט"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"המחיצה בסרגל האפליקציות"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"אפשרויות נוספות בסרגל האפליקציות"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"העברה לפינה השמאלית/העליונה"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"העברה לפינה הימנית/התחתונה"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{הצגת אפליקציה אחת (#) נוספת.}one{הצגת # אפליקציות נוספות.}two{הצגת # אפליקציות נוספות.}other{הצגת # אפליקציות נוספות.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{הצגת אפליקציה אחת (#) למחשב.}one{הצגת # אפליקציות למחשב.}two{הצגת # אפליקציות למחשב.}other{הצגת # אפליקציות למחשב.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{אפליקציה נוספת}one{אפליקציות נוספות}two{אפליקציות נוספות}other{אפליקציות נוספות}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"מחשב"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ו-<xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"בועה"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"אפשרויות נוספות"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"‫<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> מתוך <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"‫<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ועוד <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"הזזה שמאלה"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"הזזה ימינה"</string>
+    <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 4f1a162..b30b000 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"すべてクリア"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"アプリのペア設定保存"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"分割画面を使用するには、他のアプリをタップします"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"分割画面を使用するには別のアプリを選択してください"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"キャンセル"</b></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="blocked_by_policy" msgid="2071401072261365546">"この操作はアプリまたは組織で許可されていません"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"クイック設定"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"タスクバー"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"タスクバー表示"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"タスクバーとバブルを表示"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"タスクバーとバブルを右側に表示"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"タスクバー非表示"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"タスクバーとバブルを非表示"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"ナビゲーション バー"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"常にタスクバーを表示する"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ナビゲーション モードを変更"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"タスクバーの区切り"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"タスクバーのオーバフロー"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"上 / 左に移動"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"下 / 右に移動"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{他 # 件のアプリを表示できます。}other{他 # 件のアプリを表示できます。}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# 個のデスクトップ アプリが表示されます。}other{# 個のデスクトップ アプリが表示されます。}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個のその他のアプリ}other{個のその他のアプリ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"パソコン"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> と <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ふきだし"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"オーバーフロー"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>(<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>、他 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 件"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"左に移動"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"右に移動"</string>
+    <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 1fb6077..1f877e9 100644
--- a/quickstep/res/values-ka/strings.xml
+++ b/quickstep/res/values-ka/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ყველას გასუფთავება"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"აპთა წყვილის შენახვა"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"შეეხეთ სხვა აპს ეკრანის გასაყოფად"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"აირჩიეთ სხვა აპი ეკრანის გასაყოფად"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"გაუქმება"</b></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="blocked_by_policy" msgid="2071401072261365546">"ეს მოქმედება არ არის დაშვებული აპის ან თქვენი ორგანიზაციის მიერ"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"სწრაფი პარამეტრები"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ამოცანათა ზოლი"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ამოცანათა ზოლი ნაჩვენებია"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ამოცანათა ზოლი და ბუშტები ჩანს მარცხნივ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ამოცანათა ზოლი და ბუშტები ჩანს მარჯვნივ"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ამოცანათა ზოლი დამალულია"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ამოცანათა ზოლი და ბუშტები დამალულია"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"ნავიგაციის ზოლი"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ამოცანათა ზოლის მუდამ ჩვენება"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"შეცვალეთ ნავიგაციის რეჟიმი"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ამოცანათა ზოლის გამყოფი"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ამოცანათა ზოლის გადავსება"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ზემოთ/მარცხნივ გადატანა"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ქვემოთ/მარჯვნივ გადატანა"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{#-ით მეტი აპის ჩენება}other{#-ით მეტი აპის ჩვენება.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# დესკტოპის აპის ჩვენება.}other{# დესკტოპის აპის ჩვენება.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{სხვა აპი}other{სხვა აპი}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"დესკტოპი"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> და <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ბუშტი"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"გადავსება"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: <xliff:g id="APP_NAME">%2$s</xliff:g>-იდან"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> და <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> სხვა"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"მარცხნივ გადატანა"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"მარჯვნივ გადატანა"</string>
+    <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 d83e2d3..5fd172e 100644
--- a/quickstep/res/values-kk/strings.xml
+++ b/quickstep/res/values-kk/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Барлығын өшіру"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Қолданбаларды жұптауды сақтау"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Экранды бөлу режимін пайдалану үшін басқа қолданбаны түртіңіз."</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Экранды бөлу үшін басқа қолданбаны таңдаңыз."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Бас тарту"</b></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="blocked_by_policy" msgid="2071401072261365546">"Бұл әрекетке қолданба не ұйым рұқсат етпейді."</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Жылдам параметрлер"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Тапсырмалар жолағы"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Тапсырмалар жолағы көрсетілді"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Тапсырмалар жолағы мен қалқыма терезелер сол жақта көрсетілген"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Тапсырмалар жолағы мен қалқыма терезелер оң жақта көрсетілген"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Тапсырмалар жолағы жасырылды"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Тапсырмалар жолағы мен қалқыма терезелер жасырылған"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Навигация жолағы"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Тапсырма жолағын үнемі көрсету"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Навигация режимін өзгерту"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Тапсырмалар жолағын бөлгіш"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"\"Тапсырмалар жолағы\" қосымша мәзірі"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Жоғары/солға жылжыту"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмен/оңға жылжыту"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Тағы # қолданбаны көрсету.}other{Тағы # қолданбаны көрсету.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Компьютерге арналған # қолданбаны көрсету}other{Компьютерге арналған # қолданбаны көрсету}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{қосымша қолданба}other{қосымша қолданба}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Компьютер"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> және <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Қалқыма терезе"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Қосымша мәзір"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> ұсынатын <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> және тағы <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Солға жылжыту"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Оңға жылжыту"</string>
+    <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 5448433..4c8227e 100644
--- a/quickstep/res/values-km/strings.xml
+++ b/quickstep/res/values-km/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"សម្អាត​ទាំងអស់"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"រក្សាទុកគូកម្មវិធី"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ចុចកម្មវិធី​ផ្សេងទៀត ដើម្បីប្រើ​មុខងារបំបែកអេក្រង់"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"ជ្រើសរើសកម្មវិធីផ្សេងទៀត ដើម្បីប្រើមុខងារ​បំបែកអេក្រង់"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"បោះបង់"</b></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="blocked_by_policy" msgid="2071401072261365546">"សកម្មភាពនេះ​មិនត្រូវបានអនុញ្ញាតដោយកម្មវិធី​ ឬ​ស្ថាប័ន​របស់អ្នកទេ"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ការកំណត់រហ័ស"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"របារកិច្ចការ"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"បានបង្ហាញរបារកិច្ចការ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"បានបង្ហាញរបារកិច្ចការ និងផ្ទាំងអណ្ដែតនៅខាងឆ្វេង"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"បានបង្ហាញរបារកិច្ចការ និងផ្ទាំងអណ្ដែតនៅខាងស្ដាំ"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"បានលាក់របារកិច្ចការ"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"បានលាក់របារកិច្ចការ និងផ្ទាំងអណ្ដែត"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"របាររុករក"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"បង្ហាញរបារកិច្ចការជានិច្ច"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ប្ដូរ​មុខងាររុករក"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"បន្ទាត់​ខណ្ឌចែករបារកិច្ចការ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ម៉ឺនុយបន្ថែមរបារកិច្ចការ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ផ្លាស់ទីទៅខាងលើ/ឆ្វេង"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ផ្លាស់ទីទៅខាងក្រោម/ស្ដាំ"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{បង្ហាញកម្មវិធី # ទៀត។}other{បង្ហាញ​កម្មវិធី # ទៀត។}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{បង្ហាញកម្មវិធី​កុំព្យូទ័រ #។}other{បង្ហាញកម្មវិធី​កុំព្យូទ័រ #។}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{កម្មវិធីច្រើនទៀត}other{កម្មវិធីច្រើនទៀត}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"អេក្រង់ដើម"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> និង <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ផ្ទាំងសារ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ម៉ឺនុយបន្ថែម"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ពី <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> និង <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> នាក់ទៀត"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ផ្លាស់ទី​ទៅ​ឆ្វេង"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ផ្លាស់ទីទៅស្ដាំ"</string>
+    <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 74c7750..afd53ac 100644
--- a/quickstep/res/values-kn/strings.xml
+++ b/quickstep/res/values-kn/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ಎಲ್ಲವನ್ನೂ ತೆರವುಗೊಳಿಸಿ"</string>
@@ -73,13 +75,13 @@
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಗಿನ ಅಂಚಿನಿಂದ ನೀವು ಸ್ವೈಪ್ ಮಾಡುತ್ತಿದ್ದೀರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ"</string>
     <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_with_follow_up" msgid="3544611727467765026">"ಜೆಶ್ಚರ್‌ಗಳನ್ನು ಬಳಸುವುದು ಹೇಗೆಂದು ನೀವು ತಿಳಿದುಕೊಂಡಿರುವಿರಿ. ಜೆಶ್ಚರ್‌ಗಳನ್ನು ಆಫ್ ಮಾಡಲು, ಸೆಟ್ಟಿಂಗ್‌ಗಳಿಗೆ ಹೋಗಿ."</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>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"ಆ್ಯಪ್‌ಗಳನ್ನು ಬದಲಿಸಿ"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"ನಿಮ್ಮ ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಭಾಗದಿಂದ ಮೇಲಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ, ಹೋಲ್ಡ್ ಮಾಡಿ, ನಂತರ ಬಿಡುಗಡೆ ಮಾಡಿ"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"ನಿಮ್ಮ ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಭಾಗದಿಂದ ಮೇಲಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ, ಹೋಲ್ಡ್ ಮಾಡಿ, ನಂತರ ಬಿಟ್ಟುಬಿಡಿ"</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"ಭೇಷ್!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"ಸಂಪೂರ್ಣ ಸಿದ್ಧವಾಗಿದೆ"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"ಮುಗಿದಿದೆ"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ಆ್ಯಪ್ ಪೇರ್ ಸೇವ್ ಮಾಡಿ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್ ಬಳಸಲು ಇನ್ನೊಂದು ಆ್ಯಪ್ ಆಯ್ಕೆಮಾಡಿ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ರದ್ದುಮಾಡಿ"</b></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="blocked_by_policy" msgid="2071401072261365546">"ಆ್ಯಪ್ ಅಥವಾ ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ಈ ಕ್ರಿಯೆಯನ್ನು ಅನುಮತಿಸುವುದಿಲ್ಲ"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ತ್ವರಿತ ಸೆಟ್ಟಿಂಗ್‍ಗಳು"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ಟಾಸ್ಕ್‌ಬಾರ್"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ಟಾಸ್ಕ್‌ಬಾರ್ ತೋರಿಸಲಾಗಿದೆ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ಟಾಸ್ಕ್‌ಬಾರ್ &amp; ಬಬಲ್ಸ್ ತೋರಿಸಲಾಗಿದೆ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ಟಾಸ್ಕ್‌ಬಾರ್ &amp; ಬಬಲ್ಸ್ ಬಲಭಾಗದಲ್ಲಿ ತೋರಿಸಲಾಗಿದೆ"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ಟಾಸ್ಕ್‌ಬಾರ್ ಮರೆಮಾಡಲಾಗಿದೆ"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ಟಾಸ್ಕ್‌ಬಾರ್ &amp; ಬಬಲ್ಸ್ ಮರೆಮಾಡಲಾಗಿದೆ"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"ನ್ಯಾವಿಗೇಷನ್ ಬಾರ್"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ಯಾವಾಗಲೂ ಟಾಸ್ಕ್‌ಬಾರ್ ತೋರಿಸಿ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ನ್ಯಾವಿಗೇಶನ್ ಮೋಡ್ ಬದಲಾಯಿಸಿ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ಟಾಸ್ಕ್‌ಬಾರ್ ಡಿವೈಡರ್"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ಟಾಸ್ಕ್ ಬಾರ್ ಓವರ್‌ಫ್ಲೋ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ಮೇಲಿನ/ಎಡಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ಕೆಳಗಿನ/ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ಇನ್ನೂ # ಆ್ಯಪ್ ಅನ್ನು ತೋರಿಸಿ.}one{ಇನ್ನೂ # ಆ್ಯಪ್‌ಗಳನ್ನು ತೋರಿಸಿ.}other{ಇನ್ನೂ # ಆ್ಯಪ್‌ಗಳನ್ನು ತೋರಿಸಿ.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ಡೆಸ್ಕ್‌ಟಾಪ್ ಆ್ಯಪ್ ತೋರಿಸಿ.}one{# ಡೆಸ್ಕ್‌ಟಾಪ್ ಆ್ಯಪ್‌ಗಳನ್ನು ತೋರಿಸಿ.}other{# ಡೆಸ್ಕ್‌ಟಾಪ್ ಆ್ಯಪ್‌ಗಳನ್ನು ತೋರಿಸಿ.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌}one{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌ಗಳು}other{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌ಗಳು}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ಡೆಸ್ಕ್‌ಟಾಪ್"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ಮತ್ತು <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ಬಬಲ್"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ಓವರ್‌ಫ್ಲೋ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> ನಿಂದ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ಮತ್ತು ಇನ್ನೂ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ಎಡಕ್ಕೆ ಸರಿಸಿ"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ಬಲಕ್ಕೆ ಸರಿಸಿ"</string>
+    <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 c27b7f8..d602482 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"모두 삭제"</string>
@@ -90,7 +92,7 @@
     <string name="allset_title" msgid="5021126669778966707">"설정 완료"</string>
     <string name="allset_hint" msgid="459504134589971527">"위로 스와이프하여 홈으로 이동"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"홈 화면으로 이동하려면 홈 버튼을 탭하세요."</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g>을(를) 사용할 준비가 되었습니다."</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> 사용 준비가 완료되었습니다."</string>
     <string name="default_device_name" msgid="6660656727127422487">"기기"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"시스템 탐색 설정"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"공유"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"앱 페어링 저장"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"다른 앱을 탭하여 화면 분할 사용"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"화면 분할을 사용하려면 다른 앱을 선택하세요."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"취소"</b></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="blocked_by_policy" msgid="2071401072261365546">"이 작업은 앱 또는 조직에서 허용되지 않습니다."</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"빠른 설정"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"태스크 바"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"태스크 바 표시"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"태스크 바와 대화창을 왼쪽에 표시"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"태스크 바와 대화창을 오른쪽에 표시"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"태스크 바 숨김"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"태스크 바 및 대화창 숨김"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"탐색 메뉴"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"태스크 바 항상 표시"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"탐색 모드 변경"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"태스크 바 분할"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"태스크 바 오버플로"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"상단/왼쪽으로 이동"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"하단/오른쪽으로 이동"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{앱 #개 더 표시}other{앱 #개 더 표시}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{데스크톱 앱 #개를 표시합니다.}other{데스크톱 앱 #개를 표시합니다.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{추가 앱}other{추가 앱}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"데스크톱"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> 및 <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"풍선"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"더보기"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>의 <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> 외 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>개"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"왼쪽으로 이동"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"오른쪽으로 이동"</string>
+    <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 f0d2af8..e5fed79 100644
--- a/quickstep/res/values-ky/strings.xml
+++ b/quickstep/res/values-ky/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Баарын тазалоо"</string>
@@ -90,8 +92,8 @@
     <string name="allset_title" msgid="5021126669778966707">"Бүттү!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Башкы бетке өтүү үчүн экранды өйдө сүрүңүз"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Башкы экранга өтүү үчүн башкы бет баскычын таптап коюңуз"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> түзмөгүн колдоно берсеңиз болот"</string>
-    <string name="default_device_name" msgid="6660656727127422487">"түзмөк"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> колдоно берсеңиз болот"</string>
+    <string name="default_device_name" msgid="6660656727127422487">"Түзмөктү"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Өтүү аракетинин системалык параметрлери"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Бөлүшүү"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Колдонмолорду сактап коюу"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Экранды бөлүү үчүн башка колдонмону таптап коюңуз"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Экранды бөлүү үчүн башка колдонмону тандаңыз"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Жокко чыгаруу"</b></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="blocked_by_policy" msgid="2071401072261365546">"Бул аракетти аткарууга колдонмо же ишканаңыз тыюу салган"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Ыкчам параметрлер"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Тапшырмалар тактасы"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Тапшырмалар панели көрсөтүлдү"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Тапшырмалар панели, калкыма билдирмелер сол жакта көрсөтүлдү"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Тапшырмалар панели, калкыма билдирмелер оң жакта көрсөтүлгөн"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Тапшырмалар панели жашырылды"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Тапшырмалар панели жана калкып чыкма билдирмелер жашырылган"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Чабыттоо тилкеси"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Такта ар дайым көрүнсүн"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Өтүү режимин өзгөртүү"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Тапшырмалар панелин бөлгүч"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"\"Тапшырмалар панели\" кошумча менюсу"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Жогорку/сол бурчка жылдыруу"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмөнкү/оң бурчка жылдыруу"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Дагы # колдонмону көрсөтүү.}other{Дагы # колдонмону көрсөтүү.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# иш такта колдонмосун көрсөтүү.}other{# иш такта колдонмосун көрсөтүү.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{колдонмо бар}other{колдонмо бар}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Компьютер"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> жана <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Көбүкчө"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Кошумча меню"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> колдонмосунан <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> жана дагы <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Солго жылдыруу"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Оңго жылдыруу"</string>
+    <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 e862b9e..efdc7de 100644
--- a/quickstep/res/values-land/dimens.xml
+++ b/quickstep/res/values-land/dimens.xml
@@ -81,9 +81,9 @@
     <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>
 
-    <dimen name="keyboard_quick_switch_taskview_width">205dp</dimen>
-    <dimen name="keyboard_quick_switch_taskview_height">119dp</dimen>
+    <!-- Keyboard Quick Switch -->
+    <dimen name="keyboard_quick_switch_taskview_width">217.6dp</dimen>
 
 </resources>
diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml
index f54c712..2df1a49 100644
--- a/quickstep/res/values-lo/strings.xml
+++ b/quickstep/res/values-lo/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ລຶບລ້າງທັງໝົດ"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ບັນທຶກຈັບຄູ່ແອັບ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ແຕະແອັບອື່ນເພື່ອໃຊ້ໜ້າຈໍແຍກ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"ເລືອກແອັບອື່ນເພື່ອໃຊ້ການແບ່ງໜ້າຈໍ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ຍົກເລີກ"</b></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="blocked_by_policy" msgid="2071401072261365546">"ແອັບ ຫຼື ອົງການຂອງທ່ານບໍ່ອະນຸຍາດໃຫ້ໃຊ້ຄຳສັ່ງນີ້"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ການຕັ້ງຄ່າດ່ວນ"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ແຖບໜ້າວຽກ"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ແຖບໜ້າວຽກທີ່ສະແດງຢູ່"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ແຖບໜ້າວຽກ ແລະ ຟອງສະແດງຢູ່ເບື້ອງຊ້າຍ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ແຖບໜ້າວຽກ ແລະ ຟອງສະແດງຢູ່ເບື້ອງຂວາ"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ແຖບໜ້າວຽກທີ່ເຊື່ອງໄວ້ຢູ່"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ແຖບໜ້າວຽກ ແລະ ຟອງຖືກເຊື່ອງໄວ້"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"ແຖບການນຳທາງ"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ສະແດງແຖບໜ້າວຽກສະເໝີ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ປ່ຽນໂໝດການນຳທາງ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ເສັ້ນແບ່ງແຖບໜ້າວຽກ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ສ່ວນເພີ່ມເຕີມຂອງແຖບໜ້າວຽກ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ຍ້າຍໄປຊ້າຍ/ເທິງ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ຍ້າຍໄປຂວາ/ລຸ່ມ"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ສະແດງອີກ # ແອັບ.}other{ສະແດງອີກ # ແອັບ.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{ສະແດງແອັບເດັສທັອບ # ລາຍການ.}other{ສະແດງແອັບເດັສທັອບ # ລາຍການ.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ແອັບເພີ່ມເຕີມ}other{ແອັບເພີ່ມເຕີມ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ເດັສທັອບ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ແລະ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ຟອງ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ລາຍການເພີ່ມເຕີມ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ຈາກ <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ແລະ ອີກ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ລາຍການ"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ຍ້າຍໄປຊ້າຍ"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ຍ້າຍໄປຂວາ"</string>
+    <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 554745e..1d29f57 100644
--- a/quickstep/res/values-lt/strings.xml
+++ b/quickstep/res/values-lt/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Išvalyti viską"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Išsaug. progr. porą"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Išskaidyto ekrano režimas palietus kitą programą"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Išskaidyto ekrano režimą naudokite kita programa"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Atšaukti"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Atšaukti"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Išeiti iš išskaidyto ekrano pasirinkimo"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Išskaidyto ekrano režimą naudokite kita programa"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Jūsų organizacijoje arba naudojant šią programą neleidžiama atlikti šio veiksmo"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Spartieji nustatymai"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Užduočių juosta"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Užduočių juosta rodoma"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Užduočių juosta ir burbulai rodomi kairėje"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Užduočių juosta ir burbulai rodomi dešinėje"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Užduočių juosta paslėpta"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Užduočių juosta ir burbulai paslėpti"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Naršymo juosta"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Visada rodyti užduočių juostą"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Keisti naršymo režimą"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Užduočių juostos daliklis"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Užduočių juostos perpildymas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Perkelti aukštyn, kairėn"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Perkelti žemyn, dešinėn"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Rodyti dar # programą.}one{Rodyti dar # programą.}few{Rodyti dar # programas.}many{Rodyti dar # programos.}other{Rodyti dar # programų.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Rodyti # darbalaukio programą.}one{Rodyti # darbalaukio programą.}few{Rodyti # darbalaukio programas.}many{Rodyti # darbalaukio programos.}other{Rodyti # darbalaukio programų.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{papildoma programa}one{papildoma programa}few{papildomos programos}many{papildomos programos}other{papildomų programų}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Stalinis kompiuteris"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"„<xliff:g id="APP_NAME_1">%1$s</xliff:g>“ ir „<xliff:g id="APP_NAME_2">%2$s</xliff:g>“"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Burbulas"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Perpildymas"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ iš „<xliff:g id="APP_NAME">%2$s</xliff:g>“"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"„<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>“ ir dar <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Perkelti kairėn"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Perkelti dešinėn"</string>
+    <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 a6a0dab..805a598 100644
--- a/quickstep/res/values-lv/strings.xml
+++ b/quickstep/res/values-lv/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Notīrīt visu"</string>
@@ -67,7 +69,7 @@
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Vilkšana, lai pārietu uz sākumu"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Velciet augšup no ekrāna apakšdaļas. Ar šo žestu vienmēr varat atvērt sākuma ekrānu."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Ar 2 pirkstiem velciet augšup no ekrāna apakšdaļas. Ar šo žestu vienmēr varat atvērt sākuma ekrānu."</string>
-    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Došanās uz sākuma ekrānu"</string>
+    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Doties uz sākuma ekrānu"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Velciet augšup no ekrāna apakšdaļas."</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Lieliski!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Jāvelk augšup no ekrāna apakšmalas."</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Saglabāt pāri"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Lai sadalītu ekrānu, pieskarieties citai lietotnei"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Izvēlieties citu lietotni, lai sadalītu ekrānu"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Atcelt"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Atcelt"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izejiet no ekrāna sadalīšanas režīma atlases."</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Izvēlieties citu lietotni, lai sadalītu ekrānu"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Lietotne vai jūsu organizācija neatļauj veikt šo darbību."</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Ātrie iestatīj."</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Uzdevumu josla"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Uzdevumu josla tiek rādīta"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Uzdevumu josla/burbuļi pa kreisi"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Uzdevumu josla/burbuļi pa labi"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Uzdevumu josla paslēpta"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Uzdevumu josla/burbuļi paslēpti"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigācijas josla"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vienmēr rādīt uzdevumu joslu"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Mainīt navigācijas režīmu"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Uzdevumu joslas atdalītājs"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Uzdevumu joslas pārpilde"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Pārvietot uz augšējo/kreiso stūri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pārvietot uz apakšējo/labo stūri"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Rādīt vēl # lietotni}zero{Rādīt vēl # lietotnes}one{Rādīt vēl # lietotni}other{Rādīt vēl # lietotnes}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Rādīt # datora lietotni.}zero{Rādīt # datora lietotnes.}one{Rādīt # datora lietotni.}other{Rādīt # datora lietotnes.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{papildu lietotne}zero{papildu lietotņu}one{papildu lietotne}other{papildu lietotnes}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Darbvirsma"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"“<xliff:g id="APP_NAME_1">%1$s</xliff:g>” un “<xliff:g id="APP_NAME_2">%2$s</xliff:g>”"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Burbulis"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Pārpilde"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> no lietotnes <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> un vēl <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Pārvietot pa kreisi"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Pārvietot pa labi"</string>
+    <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 4859055..2634b94 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Избриши ги сите"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Зачувај го паров"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Допрете друга аплик. за да користите поделен екран"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Изберете друга апликација за да користите поделен екран"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Откажи"</b></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="blocked_by_policy" msgid="2071401072261365546">"Апликацијата или вашата организација не го дозволува дејствово"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Брзи поставки"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Лента со задачи"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Лентата со задачи е прикажана"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Лен. со зад. и бал. се лево"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Лен. со зад. и бал. се десно"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Лентата со задачи е скриена"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Лен. со зад. и бал. се скриени"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Лента за навигација"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Секогаш прикажувај „Лента“"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Променете режим на навигација"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделник на „Лента со задачи“"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Проширено балонче на „Лента со задачи“"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Премести горе лево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести долу десно"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Прикажи уште # апликација.}one{Прикажи уште # апликација.}other{Прикажи уште # апликации.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Прикажи # апликација за компјутер.}one{Прикажи # апликација за компјутер.}other{Прикажи # апликации за компјутер.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{дополнителна апликација}one{дополнителна апликација}other{дополнителни апликации}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Режим за компјутер"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Балонче"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Проширено балонче"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> од <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и уште <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Премести налево"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Премести надесно"</string>
+    <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 85b093d..92cad89 100644
--- a/quickstep/res/values-ml/strings.xml
+++ b/quickstep/res/values-ml/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"എല്ലാം മായ്‌ക്കുക"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ആപ്പ് ജോടി സംരക്ഷിക്കൂ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"സ്പ്ലിറ്റ് സ്ക്രീനിന് മറ്റൊരു ആപ്പിൽ ടാപ്പ് ചെയ്യൂ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"സ്ക്രീൻ വിഭജന മോഡ് ഉപയോഗിക്കാൻ മറ്റൊരു ആപ്പ് തിരഞ്ഞെടുക്കൂ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"റദ്ദാക്കുക"</b></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="blocked_by_policy" msgid="2071401072261365546">"ഈ നടപടി എടുക്കുന്നത് ആപ്പോ നിങ്ങളുടെ സ്ഥാപനമോ അനുവദിക്കുന്നില്ല"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ദ്രുത ക്രമീകരണം"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ടാസ്‌ക്ബാർ"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ടാസ്‌ക്ബാർ കാണിച്ചിരിക്കുന്നു"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ടാസ്ക്ബാറും ബബിളും ഇടതുവശത്ത്"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ടാസ്ക്ബാറും ബബിളും വലതുവശത്ത്"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ടാസ്‌ക്ബാർ മറച്ചിരിക്കുന്നു"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ടാസ്ക്ബാറും ബബിളും മറച്ചു"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"നാവിഗേഷൻ ബാർ"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ടാസ്‌ക്ബാർ എപ്പോഴും കാണിക്കൂ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"നാവിഗേഷൻ മോഡ് മാറ്റുക"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ടാസ്‌ക്ബാർ ഡിവൈഡർ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ടാസ്‌ക്ബാർ ഓവർഫ്ലോ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"മുകളിലേക്കോ ഇടത്തേക്കോ നീക്കുക"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"താഴേക്കോ വലത്തേക്കോ നീക്കുക"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# ആപ്പ് കൂടി കാണിക്കുക.}other{# ആപ്പുകൾ കൂടി കാണിക്കുക.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ഡെസ്‌ക്ടോപ്പ് ആപ്പ് കാണിക്കുക.}other{# ഡെസ്‌ക്ടോപ്പ് ആപ്പുകൾ കാണിക്കുക.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{കൂടുതൽ ആപ്പ്}other{കൂടുതൽ ആപ്പുകൾ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ഡെസ്‌ക്ടോപ്പ്"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g>, <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ബബിൾ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ഓവർഫ്ലോ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> എന്നതിൽ നിന്നുള്ള <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> എന്നതും മറ്റ് <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> എണ്ണവും"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ഇടത്തേക്ക് നീക്കുക"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"വലത്തേക്ക് നീക്കുക"</string>
+    <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 fe2e4a4..539e104 100644
--- a/quickstep/res/values-mn/strings.xml
+++ b/quickstep/res/values-mn/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Бүгдийг арилгах"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Апп хослуулалт хадгал"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Дэлгэцийг хуваахыг ашиглахын тулд өөр аппыг товш"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Дэлгэц хуваахыг ашиглахын тулд өөр апп сонгоно уу"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Цуцлах"</b></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="blocked_by_policy" msgid="2071401072261365546">"Энэ үйлдлийг апп эсвэл танай байгууллага зөвшөөрдөггүй"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Шуурхай тохиргоо"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Ажлын хэсэг"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Ажлын хэсгийг харуулсан"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Ажлын хэсэг, бөмбөлгийг зүүн талд харуулсан"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Ажлын хэсэг, бөмбөлгийг баруун талд харуулсан"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Ажлын хэсгийг нуусан"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Ажлын хэсэг, бөмбөлгийг нуусан"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Навигацын самбар"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ажлын хэсгийг үргэлж харуулах"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Навигацын горимыг өөрчлөх"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Ажлын хэсгийг хуваагч"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Ажлын хэсгийн урт цэс"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Зүүн дээд хэсэг рүү зөөх"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Баруун доод хэсэг рүү зөөх"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Өөр # аппыг харуулна уу.}other{Өөр # аппыг харуулна уу.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Компьютерын # аппыг харуулна уу.}other{Компьютерын # аппыг харуулна уу.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{бусад апп}other{бусад апп}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Дэлгэц"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> болон <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Бөмбөлөг"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Илүү хэсэг"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>-с ирсэн <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> болон бусад <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Зүүн тийш зөөх"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Баруун тийш зөөх"</string>
+    <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 b053a21..dd2003f 100644
--- a/quickstep/res/values-mr/strings.xml
+++ b/quickstep/res/values-mr/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"सर्व साफ करा"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ॲपची जोडी सेव्ह करा"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिट स्क्रीन वापरण्यासाठी दुसऱ्या ॲपवर टॅप करा"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"स्प्लिट स्क्रीन वापरण्यासाठी दुसरे ॲप निवडा"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"रद्द करा"</b></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="blocked_by_policy" msgid="2071401072261365546">"अ‍ॅप किंवा तुमच्या संस्थेद्वारे ही क्रिया करण्याची अनुमती नाही"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"क्विक सेटिंग्ज"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"टास्कबार"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"टास्कबार दाखवलेला आहे"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"टास्कबार आणि डावे बबल दाखवले"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"टास्कबार आणि उजवे बबल लपवले"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"टास्कबार लपवलेले आहे"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"टास्कबार आणि बबल लपवले आहेत"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"नेव्हिगेशन बार"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"नेहमी टास्कबार दाखवा"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"नेव्हिगेशन मोड बदला"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"टास्कबार विभाजक"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"टास्कबार ओव्हरफ्लो"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"सर्वात वरती/डावीकडे हलवा"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"तळाशी/उजवीकडे हलवा"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{आणखी # अ‍ॅप दाखवा.}other{आणखी # अ‍ॅप्स दाखवा.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# डेस्‍कटॉप अ‍ॅप दाखवा.}other{# डेस्‍कटॉप अ‍ॅप्स दाखवा.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{आणखी अ‍ॅप}other{आणखी अ‍ॅप्स}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"डेस्कटॉप"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> आणि <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"बबल"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ओव्हरफ्लो"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> वरील <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> आणि आणखी <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"डावीकडे हलवा"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"उजवीकडे हलवा"</string>
+    <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 c0219e0..af388bc 100644
--- a/quickstep/res/values-ms/strings.xml
+++ b/quickstep/res/values-ms/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Kosongkan semua"</string>
@@ -68,18 +70,18 @@
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Leret ke atas dari bahagian bawah skrin. Gerak isyarat ini sentiasa membawa anda ke Skrin utama."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Leret ke atas dengan 2 jari dari bawah skrin. Gerak isyarat ini sentiasa bawa anda ke Skrin utama."</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Pergi ke skrin utama"</string>
-    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Leret ke atas dari bahagian bawah skrin anda"</string>
+    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Leret ke atas dari bahagian bawah skrin"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Syabas!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Pastikan anda meleret ke atas dari sisi bahagian bawah skrin"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Cuba tahan tetingkap untuk tempoh yang lebih lama sebelum melepaskan"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Pastikan anda meleret terus ke atas, kemudian menjeda"</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Pastikan anda meleret lurus ke atas, kemudian berhenti seketika"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Anda sudah belajar cara menggunakan gerak isyarat. Untuk mematikan gerak isyarat, pergi ke Tetapan."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Anda telah melengkapkan gerak isyarat menukar apl"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Leret untuk menukar apl"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Untuk beralih antara apl, leret ke atas dari bahagian bawah skrin anda, tahan, kemudian lepaskan."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Untuk beralih antara apl, leret ke atas dengan 2 jari dari bawah skrin, tahan, kemudian lepaskan."</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Beralih antara apl"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Leret ke atas dari bahagian bawah skrin anda, tahan, kemudian lepaskan"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Leret ke atas dari bahagian bawah skrin, tahan, kemudian lepas"</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Syabas!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Selesai"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Selesai"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Simpan gandingan apl"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ketik apl lain untuk menggunakan skrin pisah"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Pilih apl lain untuk menggunakan skrin pisah"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Batal"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Batal"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Keluar daripada pilihan skrin pisah"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pilih apl lain untuk menggunakan skrin pisah"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Tindakan ini tidak dibenarkan oleh apl atau organisasi anda"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Tetapan Pantas"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Bar Tugas"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Bar Tugas dipaparkan"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Bar Tugas &amp; gelembung dipaparkan di sebelah kiri"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Bar Tugas &amp; gelembung dipaparkan di sebelah kanan"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Bar Tugas disembunyikan"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Bar Tugas &amp; gelembung disembunyikan"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Bar navigasi"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Papar Bar Tugas selalu"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Tukar mod navigasi"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Pembahagi Bar Tugas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Limpahan Bar Tugas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Alihkan ke atas/kiri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Alihkan ke bawah/kanan"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Tunjukkan # lagi apl.}other{Tunjukkan # lagi apl.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Tunjukkan # apl desktop.}other{Tunjukkan # apl desktop.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{apl lagi}other{apl lagi}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> dan <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Gelembung"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Limpahan"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> daripada <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> dan <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> lagi"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Alih ke kiri"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Alih ke kanan"</string>
+    <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 7c7ff82..55b65c8 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"အားလုံးရှင်းရန်"</string>
@@ -47,7 +49,7 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"ကြိုတင်မှန်းဆထားသော အက်ပ်− <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"သင့်စက်ကို လှည့်ပါ"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"လက်ဟန်ဖြင့် လမ်းညွှန်ခြင်း ရှင်းလင်းပို့ချချက်အား အပြီးသတ်ရန် သင့်စက်ကို လှည့်ပါ"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"ညာ (သို့) ဘယ်အစွန်း၏ ခပ်လှမ်းလှမ်းမှ ပွတ်ဆွဲကြောင်း သေချာပါစေ"</string>
+    <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>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"အက်ပ်အတွဲ သိမ်းရန်"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"မျက်နှာပြင် ခွဲ၍ပြသရန် အက်ပ်နောက်တစ်ခုကို တို့ပါ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းသုံးရန် နောက်အက်ပ်တစ်ခုရွေးပါ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"မလုပ်တော့"</b></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="blocked_by_policy" msgid="2071401072261365546">"ဤလုပ်ဆောင်ချက်ကို အက်ပ် သို့မဟုတ် သင်၏အဖွဲ့အစည်းက ခွင့်မပြုပါ"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"အမြန်ဆက်တင်များ"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"လုပ်ဆောင်စရာဘား"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taskbar ပြထားသည်"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ဘယ်ဘက်၌ Taskbar နှင့် ပူဖောင်းကွက် ပြထားသည်"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ညာဘက်၌ Taskbar နှင့် ပူဖောင်းကွက် ပြထားသည်"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taskbar ဖျောက်ထားသည်"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taskbar နှင့် ပူဖောင်းကွက် ဖျောက်ထားသည်"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"လမ်းညွှန်ဘား"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Taskbar အမြဲပြရန်"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ရွှေ့ကြည့်သည့်မုဒ် ပြောင်းရန်"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"လုပ်ဆောင်စရာဘား ပိုင်းခြားစနစ်"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar မီနူးအပို"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"အပေါ်/ဘယ်ဘက်သို့ ရွှေ့ရန်"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"အောက်ခြေ/ညာဘက်သို့ ရွှေ့ရန်"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{နောက်ထပ်အက်ပ် # ခု ပြပါ။}other{နောက်ထပ်အက်ပ် # ခု ပြပါ။}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{ဒက်စတော့ အက်ပ် # ခု ပြပါ။}other{ဒက်စတော့ အက်ပ် # ခု ပြပါ။}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{နောက်ထပ်အက်ပ်}other{နောက်ထပ်အက်ပ်များ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ဒက်စ်တော့"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> နှင့် <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ပူဖောင်းကွက်"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"မီနူးအပို"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> မှ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> နှင့် နောက်ထပ် <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ခု"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ဘယ်သို့ရွှေ့ရန်"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ညာသို့ရွှေ့ရန်"</string>
+    <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 6aa755a..077bc0a 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Fjern alt"</string>
@@ -96,10 +98,10 @@
     <string name="action_share" msgid="2648470652637092375">"Del"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Skjermbilde"</string>
     <string name="action_split" msgid="2098009717623550676">"Del opp"</string>
-    <string name="action_save_app_pair" msgid="5974823919237645229">"Lagre apptilkobling"</string>
+    <string name="action_save_app_pair" msgid="5974823919237645229">"Lagre app-paret"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Trykk på en annen app for å bruke delt skjerm"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Velg en annen app for å bruke delt skjerm"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Avbryt"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Avbryt"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Avslutt valg av delt skjerm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Velg en annen app for å bruke delt skjerm"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller organisasjonen din tillater ikke denne handlingen"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Hurtiginnst."</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Oppgavelinje"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Oppgavelinjen vises"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Venstre oppgavelinje/boble vises"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Høyre oppgavelinje/bobler vises"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Oppgavelinjen er skjult"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Oppgavelinje og bobler skjult"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigasjonsrad"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vis alltid oppgavelinjen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Endre navigasjonsmodus"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Skille for oppgavelinjen"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Overflyt for oppgavelinjen"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flytt til øverst/venstre"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytt til nederst/høyre"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Vis # app til.}other{Vis # apper til.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Vis # datamaskinprogram.}other{Vis # datamaskinprogrammer.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app til}other{apper til}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Datamaskin"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> og <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Boble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflyt"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> andre"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Flytt til venstre"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Flytt til høyre"</string>
+    <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 d49fd2d..0f374ba 100644
--- a/quickstep/res/values-ne/strings.xml
+++ b/quickstep/res/values-ne/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"सबै मेटाउनुहोस्"</string>
@@ -50,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>
@@ -62,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>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"एपको पेयर सेभ गर्नुहोस्"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिटस्क्रिन प्रयोग गर्न अर्को एपमा ट्याप गर्नु…"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"स्प्लिट स्क्रिन प्रयोग गर्न अर्को एप रोज्नुहोस्"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"रद्द गर्नुहोस्"</b></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="blocked_by_policy" msgid="2071401072261365546">"यो एप वा तपाईंको सङ्गठनले यो कारबाही गर्ने अनुमति दिँदैन"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"द्रुत सेटिङ"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"टास्कबार"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"टास्कबार देखाइएको छ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"टास्कबार र बबल बार बायाँतिर देखाइएका छन्"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"टास्कबार र बबल बार दायाँतिर देखाइएका छन्"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"टास्कबार लुकाइएको छ"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"टास्कबार र बबल बार लुकाइएका छन्"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"नेभिगेसन बार"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"टास्कबार सधैँ देखाउनुहोस्"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"नेभिगेसन मोड बदल्नुहोस्"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"टास्कबार डिभाइडर"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"टास्कबार ओभरफ्लो"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"सिरान/बायाँतिर सार्नुहोस्"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"फेद/दायाँतिर सार्नुहोस्"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{थप # एप देखाउनुहोस्।}other{थप # वटा एप देखाउनुहोस्।}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# डेस्कटप एप देखाउनुहोस्।}other{# वटा डेस्कटप एप देखाउनुहोस्।}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{थप एप}other{थप एपहरू}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"डेस्कटप"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> र <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"बबल"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ओभरफ्लो"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> मा देखाइएका <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> र थप <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"बायाँतिर सार्नुहोस्"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"दायाँतिर सार्नुहोस्"</string>
+    <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 94100ba..a1e9c70 100644
--- a/quickstep/res/values-night/colors.xml
+++ b/quickstep/res/values-night/colors.xml
@@ -25,7 +25,5 @@
     <color name="all_set_page_background">@android:color/system_neutral1_900</color>
 
     <!-- Turn on work apps button -->
-    <color name="work_turn_on_stroke">?androidprv:attr/colorAccentSecondaryVariant</color>
-    <color name="work_fab_bg_color">?androidprv:attr/materialColorPrimaryFixedDim</color>
-    <color name="work_fab_icon_color">?androidprv:attr/materialColorOnPrimaryFixed</color>
+    <color name="work_turn_on_stroke">?attr/materialColorPrimary</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values-night/styles.xml b/quickstep/res/values-night/styles.xml
index 401351f..eb88310 100644
--- a/quickstep/res/values-night/styles.xml
+++ b/quickstep/res/values-night/styles.xml
@@ -14,16 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-
-    <style name="AllSetTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
-        <item name="android:navigationBarColor">@android:color/transparent</item>
-        <item name="android:statusBarColor">@android:color/transparent</item>
-        <item name="android:enforceNavigationBarContrast">false</item>
-        <item name="android:windowLightStatusBar">false</item>
-        <item name="android:windowBackground">@android:color/transparent</item>
-    </style>
+<resources>
 
     <style name="TextAppearance.GestureTutorial.MainTitle.Home"
         parent="TextAppearance.GestureTutorial.MainTitle">
@@ -82,16 +73,16 @@
     <style name="GestureTutorialActivity" parent="@style/AppTheme">
         <item name="background">@android:color/transparent</item>
         <item name="tutorialSubtitle">@android:color/white</item>
-        <item name="surfaceContainer">?androidprv:attr/materialColorSurfaceContainer</item>
-        <item name="onSurfaceHome">?androidprv:attr/materialColorPrimaryFixedDim</item>
-        <item name="surfaceHome">?androidprv:attr/materialColorOnPrimaryFixedVariant</item>
-        <item name="secondaryHome">?androidprv:attr/materialColorOnPrimaryFixed</item>
-        <item name="onSurfaceBack">?androidprv:attr/materialColorTertiaryFixedDim</item>
-        <item name="surfaceBack">?androidprv:attr/materialColorOnTertiaryFixedVariant</item>
-        <item name="secondaryBack">?androidprv:attr/materialColorOnTertiaryFixed</item>
-        <item name="onSurfaceOverview">?androidprv:attr/materialColorPrimaryFixed</item>
-        <item name="surfaceOverview">?androidprv:attr/materialColorOnSecondaryFixedVariant</item>
-        <item name="secondaryOverview">?androidprv:attr/materialColorOnSecondaryFixed</item>
+        <item name="surfaceContainer">?attr/materialColorSurfaceContainer</item>
+        <item name="onSurfaceHome">?attr/materialColorPrimaryFixedDim</item>
+        <item name="surfaceHome">?attr/materialColorOnPrimaryFixedVariant</item>
+        <item name="secondaryHome">?attr/materialColorOnPrimaryFixed</item>
+        <item name="onSurfaceBack">?attr/materialColorTertiaryFixedDim</item>
+        <item name="surfaceBack">?attr/materialColorOnTertiaryFixedVariant</item>
+        <item name="secondaryBack">?attr/materialColorOnTertiaryFixed</item>
+        <item name="onSurfaceOverview">?attr/materialColorPrimaryFixed</item>
+        <item name="surfaceOverview">?attr/materialColorOnSecondaryFixedVariant</item>
+        <item name="secondaryOverview">?attr/materialColorOnSecondaryFixed</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml
index ca44a69..529516c 100644
--- a/quickstep/res/values-nl/strings.xml
+++ b/quickstep/res/values-nl/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Alles wissen"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"App-paar opslaan"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tik op nog een app om je scherm te splitsen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Kies een andere app om gesplitst scherm te gebruiken"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Annuleren"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Annuleren"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Sluit de selectie voor gesplitst scherm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Kies andere app om gesplitst scherm te gebruiken"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Deze actie wordt niet toegestaan door de app of je organisatie"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Snelle instellingen"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taakbalk"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Taakbalk wordt getoond"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taakbalk en bubbels links getoond"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taakbalk en bubbels rechts getoond"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Taakbalk is verborgen"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Taakbalk en bubbels verborgen"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigatiebalk"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Taakbalk altijd tonen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigatiemodus wijzigen"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Scheiding voor Taakbalk"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taakbalkoverloop"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Naar boven/links verplaatsen"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Naar beneden/rechts verplaatsen"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Nog # app tonen.}other{Nog # apps tonen.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# desktop-app tonen.}other{# desktop-apps tonen.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{extra app}other{extra apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> en <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubbel"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overloop"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> van <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> en nog <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Naar links verplaatsen"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Naar rechts verplaatsen"</string>
+    <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 bf0bdc8..afc909d 100644
--- a/quickstep/res/values-or/strings.xml
+++ b/quickstep/res/values-or/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ସବୁ ଖାଲି କରନ୍ତୁ"</string>
@@ -47,7 +49,7 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"ପୂର୍ବାନୁମାନ କରାଯାଇଥିବା ଆପ୍: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"ଆପଣଙ୍କ ଡିଭାଇସକୁ ରୋଟେଟ କରନ୍ତୁ"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"ଜେଶ୍ଚର ନାଭିଗେସନ ଟ୍ୟୁଟୋରିଆଲ ସମ୍ପୂର୍ଣ୍ଣ କରିବାକୁ ଦୟାକରି ଆପଣଙ୍କ ଡିଭାଇସ ରୋଟେଟ କରନ୍ତୁ"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"ଆପଣ ସ୍କ୍ରିନର ଏକଦମ୍-ଡାହାଣ ବା ବାମ ଧାରରୁ ସ୍ୱାଇପ୍ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।"</string>
+    <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>
@@ -72,14 +74,14 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"ବଢ଼ିଆ କାମ!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"ଆପଣ ସ୍କ୍ରିନର ତଳ ଧାରରୁ ଉପରକୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
     <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_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_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>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"ଆପ୍ସ ସୁଇଚ କରନ୍ତୁ"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"ଆପଣଙ୍କ ସ୍କ୍ରିନ୍‌ର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ୍ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ୍ କରନ୍ତୁ"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ କରନ୍ତୁ"</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"ବହୁତ ବଢ଼ିଆ!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"ସବୁ ପ୍ରସ୍ତୁତ"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"ହୋଇଗଲା"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ଆପ ପେୟାର ସେଭ କରନ୍ତୁ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ସ୍ପ୍ଲିଟସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପରେ ଟାପ କର"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ବାତିଲ କରନ୍ତୁ"</b></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="blocked_by_policy" msgid="2071401072261365546">"ଆପ୍ କିମ୍ବା ଆପଣଙ୍କ ସଂସ୍ଥା ଦ୍ୱାରା ଏହି କାର୍ଯ୍ୟକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"କୁଇକ ସେଟିଂସ"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ଟାସ୍କବାର"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ଟାସ୍କବାର ଦେଖାଯାଇଛି"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ଟାସ୍କବାର ଓ ବବଲ ବାମରେ ଦେଖାଯାଇଛି"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ଟାସ୍କବାର ଓ ବବଲ ଡାହାଣରେ ଦେଖାଯାଇଛି"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ଟାସ୍କବାର ଲୁଚାଯାଇଛି"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ଟାସ୍କବାର ଓ ବବଲ ଲୁଚାଯାଇଛି"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"ନାଭିଗେସନ ବାର"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ସର୍ବଦା ଟାସ୍କବାର ଦେଖାନ୍ତୁ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ନାଭିଗେସନ ମୋଡ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ଟାସ୍କବାର ଡିଭାଇଡର"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ଟାସ୍କବାର ଓଭରଫ୍ଲୋ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ଶୀର୍ଷ/ବାମକୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ନିମ୍ନ/ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ଅଧିକ #ଟି ଆପ ଦେଖାନ୍ତୁ।}other{ଅଧିକ #ଟି ଆପ୍ସ ଦେଖାନ୍ତୁ।}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ଡେସ୍କଟପ ଆପ ଦେଖାନ୍ତୁ।}other{# ଡେସ୍କଟପ ଆପ୍ସ ଦେଖାନ୍ତୁ।}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ଅଧିକ ଆପ}other{ଅଧିକ ଆପ୍ସ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ଡେସ୍କଟପ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ଏବଂ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ବବଲ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ଓଭରଫ୍ଲୋ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>ରୁ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ଏବଂ ଅଧିକ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ବାମକୁ ମୁଭ କରନ୍ତୁ"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string>
+    <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 fc60396..69b33f9 100644
--- a/quickstep/res/values-pa/strings.xml
+++ b/quickstep/res/values-pa/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ਸਭ ਕਲੀਅਰ ਕਰੋ"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ਐਪ ਜੋੜਾਬੱਧ ਰੱਖਿਅਤ ਕਰੋ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨੂੰ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ \'ਤੇ ਟੈਪ ਕਰੋ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ਰੱਦ ਕਰੋ"</b></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="blocked_by_policy" msgid="2071401072261365546">"ਐਪ ਜਾਂ ਤੁਹਾਡੀ ਸੰਸਥਾ ਵੱਲੋਂ ਇਸ ਕਾਰਵਾਈ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ਤਤਕਾਲ ਸੈਟਿੰਗਾਂ"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ਟਾਸਕਬਾਰ"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ਟਾਸਕਬਾਰ ਨੂੰ ਦਿਖਾਇਆ ਗਿਆ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਖੱਬੇ ਦਿਖਾਇਆ"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਸੱਜੇ ਦਿਖਾਇਆ"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ਟਾਸਕਬਾਰ ਨੂੰ ਲੁਕਾਇਆ ਗਿਆ"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ਟਾਸਕਬਾਰ ਅਤੇ ਬਬਲ ਨੂੰ ਲੁਕਾਇਆ ਗਿਆ"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"ਨੈਵੀਗੇਸ਼ਨ ਵਾਲੀ ਪੱਟੀ"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ਹਮੇਸ਼ਾਂ ਟਾਸਕਬਾਰ ਦਿਖਾਓ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ਨੈਵੀਗੇਸ਼ਨ ਮੋਡ ਬਦਲੋ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ਟਾਸਕਬਾਰ ਵਿਭਾਜਕ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ਟਾਸਕਬਾਰ ਓਵਰਫ਼ਲੋ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ਸਿਖਰਲੇ/ਖੱਬੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ਹੇਠਾਂ/ਸੱਜੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}one{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}other{# ਹੋਰ ਐਪਾਂ ਦਿਖਾਓ।}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}one{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}other{# ਡੈਸਕਟਾਪ ਐਪਾਂ ਦਿਖਾਓ।}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ਹੋਰ ਐਪ}one{ਹੋਰ ਐਪ}other{ਹੋਰ ਐਪਾਂ}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ਡੈਸਕਟਾਪ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ਅਤੇ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ਬਬਲ"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ਓਵਰਫ਼ਲੋ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> ਤੋਂ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ਅਤੇ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ਹੋਰ"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ਖੱਬੇ ਲਿਜਾਓ"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ਸੱਜੇ ਲਿਜਾਓ"</string>
+    <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 d88e28a..88b5053 100644
--- a/quickstep/res/values-pl/strings.xml
+++ b/quickstep/res/values-pl/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Wyczyść wszystko"</string>
@@ -91,7 +93,7 @@
     <string name="allset_hint" msgid="459504134589971527">"Aby przejść na stronę główną, przesuń w górę"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Kliknij przycisk ekranu głównego, aby otworzyć ekran główny"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> jest gotowe – możesz zacząć z niego korzystać"</string>
-    <string name="default_device_name" msgid="6660656727127422487">"urządzenie"</string>
+    <string name="default_device_name" msgid="6660656727127422487">"Urządzenie"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Ustawienia nawigacji w systemie"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Udostępnij"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Zrzut ekranu"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Zapisz parę"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Aby podzielić ekran, kliknij drugą aplikację"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Aby podzielić ekran, wybierz drugą aplikację"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Anuluj"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Anuluj"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Wyjdź z wyboru podzielonego ekranu"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Wybierz drugą aplikację, aby podzielić ekran"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Nie możesz wykonać tego działania, bo nie zezwala na to aplikacja lub Twoja organizacja"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Szybkie ustawienia"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Pasek aplikacji"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Pasek aplikacji widoczny"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Pasek i dymki po lewej"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Pasek i dymki po prawej"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Pasek aplikacji ukryty"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Pasek i dymki są ukryte"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Pasek nawigacyjny"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Zawsze pokazuj pasek aplikacji"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Zmień tryb nawigacji"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Linia dzielenia paska aplikacji"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Rozwijany pasek aplikacji"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Przesuń w górny lewy róg"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Przesuń w dolny prawy róg"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Pokaż jeszcze # aplikację.}few{Pokaż jeszcze # aplikacje.}many{Pokaż jeszcze # aplikacji.}other{Pokaż jeszcze # aplikacji.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Pokaż # aplikację komputerową.}few{Pokaż # aplikacje komputerowe.}many{Pokaż # aplikacji komputerowych.}other{Pokaż # aplikacji komputerowej.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{inna aplikacja}few{inne aplikacje}many{innych aplikacji}other{innej aplikacji}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Pulpit"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Dymek"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Rozwijany"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikacji <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i jeszcze <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Przenieś w lewo"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Przenieś w prawo"</string>
+    <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 e4d07bd..84120a1 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Limpar tudo"</string>
@@ -47,7 +49,7 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"App prevista: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Rode o dispositivo"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Rode o seu dispositivo para concluir o tutorial de navegação por gestos"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Deslize rapidamente a partir da extremidade mais à direita ou mais à esquerda"</string>
+    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Deslize a partir da extremidade mais à direita ou mais à esquerda"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Deslize rapidamente a partir da extremidade esquerda ou direita até ao centro do ecrã e solte"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Aprendeu a deslizar a partir da direita para retroceder. A seguir, saiba como alternar entre apps."</string>
     <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Concluiu o gesto para retroceder. A seguir, saiba como alternar entre apps."</string>
@@ -58,8 +60,8 @@
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"Para voltar ao último ecrã, deslize rapidamente do limite esquerdo ou direito até ao centro do ecrã."</string>
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Para voltar ao último ecrã, deslize rapidamente com 2 dedos a partir da extremidade esquerda ou direita até ao centro do ecrã."</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Retroceder"</string>
-    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Deslize rapidamente a partir da extremidade esquerda ou direita para o meio do ecrã"</string>
-    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Deslize rapidamente com o dedo a partir do limite inferior do ecrã"</string>
+    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Deslize a partir da extremidade esquerda ou direita até ao centro do ecrã"</string>
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Deslize a partir do limite inferior do ecrã"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Não faça uma pausa antes de soltar"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Deslize rapidamente com o dedo para cima"</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Concluiu o gesto para aceder ao ecrã principal. A seguir, saiba como retroceder."</string>
@@ -68,18 +70,18 @@
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Deslize rapidamente para cima a partir da parte inferior. Este gesto abre sempre o ecrã principal."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Deslize rapidamente para cima com 2 dedos no fundo do ecrã. Este gesto abre sempre o ecrã principal."</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Aceda ao ecrã principal"</string>
-    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Deslize rapidamente para cima a partir da parte inferior do ecrã"</string>
+    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Deslize para cima a partir da parte inferior do ecrã"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Muito bem!"</string>
-    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Deslize rapidamente com o dedo a partir do limite inferior do ecrã"</string>
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Deslize a partir do limite inferior do ecrã"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Experimente premir a janela durante mais tempo antes de soltar"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Garanta que desliza rapidamente com o dedo para cima e, em seguida, faz uma pausa"</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Deslize para cima e pause"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Aprendeu a utilizar gestos. Para desativar os gestos, aceda às Definições."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Concluiu o gesto para alternar entre apps"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Deslize rapidamente com o dedo para alternar entre apps"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Para alternar entre apps, deslize para cima sem soltar a partir da parte inferior do ecrã e solte."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Para mudar de app, deslize rapidamente para cima com 2 dedos sem soltar no fundo do ecrã e solte."</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Mude de app"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Deslize rapidamente para cima a partir da parte inferior do ecrã sem soltar e, em seguida, solte"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Deslize para cima a partir da parte inferior do ecrã , detenha o gesto e solte"</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Muito bem!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Está tudo pronto"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Concluído"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Guardar par de apps"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toque noutra app para usar o ecrã dividido"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Escolha outra app para usar o ecrã dividido"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancelar"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancelar"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Saia da seleção de ecrã dividido"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolher outra app para usar o ecrã dividido"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Esta ação não é permitida pela app ou a sua entidade."</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Definiç. rápidas"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barra de tarefas"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Barra de tarefas apresentada"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barra de tarefas/balões à esq."</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barra de tarefas/balões à dir."</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Barra de tarefas ocultada"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barra tarefas/balões ocultos"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barra de navegação"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ver sempre Barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Alterar modo de navegação"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Menu adicional da Barra de tarefas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover para a parte superior esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para a part superior direita"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar mais # app.}other{Mostrar mais # apps.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Mostrar # app para computador.}other{Mostrar # apps para computador.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{outra app}other{outras apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Computador"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Balão"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Menu adicional"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> da app <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e mais <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> pessoas"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Mover para a esquerda"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Mover para a direita"</string>
+    <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 4fec4f8..3238c99 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Remover tudo"</string>
@@ -72,7 +74,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Muito bem!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Deslize da borda inferior da tela para cima"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Mantenha a janela pressionada por mais tempo antes de soltar"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Deslize para cima e pare"</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Deslize para cima em linha reta e pare"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Você aprendeu. Para desativar os gestos, acesse as Configurações."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Você concluiu o gesto para mudar de app"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Deslizar para trocar de app"</string>
@@ -89,7 +91,7 @@
     <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">"Tudo pronto!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Deslize para cima para acessar a tela inicial"</string>
-    <string name="allset_button_hint" msgid="2395219947744706291">"Toque no botão home para ir para a tela inicial"</string>
+    <string name="allset_button_hint" msgid="2395219947744706291">"Toque no botão home para acessar a tela inicial"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Você já pode começar a usar seu <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"dispositivo"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Configurações de navegação do sistema"</annotation></string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Salvar par de apps"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toque em outro app para usar a tela dividida"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Escolha outro app para usar na tela dividida"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Cancelar"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancelar"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Sair da seleção de tela dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolha outro app para usar na tela dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Essa ação não é permitida pelo app ou pela organização"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Config. rápidas"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Barra de tarefas"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Barra de tarefas visível"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Barra de tar. e balões à esq."</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Barra de tar. e balões à dir."</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Barra de tarefas oculta"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Barra de tar. e balões ocultos"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Barra de navegação"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Sempre mostrar a Barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Mudar o modo de navegação"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Separador da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tarefas flutuante"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover para cima/para a esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para baixo/para a direita"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar mais # app.}one{Mostrar mais # app.}other{Mostrar mais # apps.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Mostrar # app para computador.}one{Mostrar # app para computador.}other{Mostrar # apps para computador.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{outro app}one{outro app}other{outros apps}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Computador"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Balão"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Balão flutuante"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> do app <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e mais <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Mover para esquerda"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Mover para direita"</string>
+    <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 c839602..e6aad47 100644
--- a/quickstep/res/values-ro/strings.xml
+++ b/quickstep/res/values-ro/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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">"Computer"</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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Șterge tot"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Salvează perechea de aplicații"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Atinge altă aplicație pentru ecranul împărțit"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Alege altă aplicație pentru a folosi ecranul împărțit"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Anulează"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Anulează"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ieși din selecția cu ecran împărțit"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Alege altă aplicație pentru ecranul împărțit"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Această acțiune nu este permisă de aplicație sau de organizația ta"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Setări rapide"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Bară de activități"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Bara de activități este afișată"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Bară și baloane stânga afișate"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Bară &amp; baloane dreapta afișate"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Bara de activități este ascunsă"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Bară și baloane ascunse"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Bară de navigare"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Afișează mereu bara"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Schimbă modul de navigare"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Separator pentru bara de activități"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Meniu suplimentar pentru bara de activități"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mută în stânga sus"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mută în dreapta jos"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afișează încă # aplicație}few{Afișează încă # aplicații}other{Afișează încă # de aplicații}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Afișează # aplicație pentru computer.}few{Afișează # aplicații pentru computer.}other{Afișează # de aplicații pentru computer.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicație suplimentară}few{mai multe aplicații}other{mai multe aplicații}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Computer"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> și <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Balon"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Suplimentar"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> și încă <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Deplasează spre stânga"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Deplasează spre dreapta"</string>
+    <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 da49ad3..297ae02 100644
--- a/quickstep/res/values-ru/strings.xml
+++ b/quickstep/res/values-ru/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Очистить все"</string>
@@ -72,7 +74,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"У вас получилось!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Проведите снизу вверх от самого края экрана."</string>
     <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_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_intro_title" msgid="2902054412868489378">"Переключение между приложениями"</string>
@@ -92,14 +94,14 @@
     <string name="allset_button_hint" msgid="2395219947744706291">"Нажмите кнопку главного экрана, чтобы открыть его."</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Теперь вы можете использовать <xliff:g id="DEVICE">%1$s</xliff:g>."</string>
     <string name="default_device_name" msgid="6660656727127422487">"устройство"</string>
-    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Системные настройки навигации"</annotation></string>
+    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Настройки навигации в системе"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Поделиться"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
     <string name="action_split" msgid="2098009717623550676">"Разделить"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"Сохранить приложения"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Для разделения экрана выберите другое приложение."</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Чтобы использовать разделенный экран, выберите другое приложение."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Отмена"</b></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="blocked_by_policy" msgid="2071401072261365546">"Это действие заблокировано приложением или организацией."</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Быстрые настройки"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Панель задач"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Панель задач показана"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Слева панель задач, подсказки"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Справа панель задач, подсказки"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Панель задач скрыта"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Скрыты панель задач, подсказки"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Панель навигации"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Всегда показывать панель задач"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Изменить режим навигации"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделитель панели задач"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Дополнительное меню панели задач"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Переместить вверх или влево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Переместить вниз или вправо"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Показать ещё # приложение}one{Показать ещё # приложение}few{Показать ещё # приложения}many{Показать ещё # приложений}other{Показать ещё # приложения}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Показать # компьютерное приложение.}one{Показать # компьютерное приложение.}few{Показать # компьютерных приложения.}many{Показать # компьютерных приложений.}other{Показать # компьютерного приложения.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{дополнительное приложение}one{дополнительное приложение}few{дополнительных приложения}many{дополнительных приложений}other{дополнительного приложения}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Режим компьютера"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Всплывающая подсказка"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Дополнительное меню"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"\"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" из приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и ещё <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Переместить влево"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Переместить вправо"</string>
+    <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 9cbe837..2c7c672 100644
--- a/quickstep/res/values-si/strings.xml
+++ b/quickstep/res/values-si/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"සියල්ල හිස් කරන්න"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"යෙදුම් යුගල සුරකින්න"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"බෙදුම් තිරය භාවිතා කිරීමට තවත් යෙදුමක් තට්ටු කරන්න"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"බෙදුම් තිරය භාවිත කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"අවලංගු කරන්න"</b></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="blocked_by_policy" msgid="2071401072261365546">"මෙම ක්‍රියාව යෙදුම හෝ ඔබේ සංවිධානය මගින් ඉඩ නොදේ"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"ඉක්මන් සැකසීම්"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"කාර්ය තීරුව"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"කාර්ය තීරුව පෙන්වා ඇත"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"කාර්ය තීරුව සහ බුබුළු පෙන්වා ඇත"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"කාර්ය තීරුව සහ බුබුළු දකුණට පෙන්වා ඇත"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"කාර්ය තීරුව සඟවා ඇත"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"කාර්ය තීරුව සහ බුබුළු සඟවා ඇත"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"සංචලන තීරුව"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"සෑම විටම කාර්ය තීරුව පෙන්වන්න"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"සංචාලන ප්‍රකාරය වෙනස් කරන්න"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"කාර්ය තීරු බෙදනය"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"කාර්ය තීරුව පිටාර යාම"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ඉහළ/වම වෙත ගෙන යන්න"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"පහළ/දකුණ වෙත ගෙන යන්න"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{තවත් # යෙදුමක් පෙන්වන්න.}one{තවත් යෙදුම් #ක් පෙන්වන්න.}other{තවත් යෙදුම් #ක් පෙන්වන්න.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ඩෙස්ක්ටොප් යෙදුමක් පෙන්වන්න.}one{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}other{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{තව යෙදුම}one{තවත් යෙදුම්}other{තවත් යෙදුම්}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ඩෙස්ක්ටොපය"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> සහ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"බුබුළු"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"පිටාර යාම"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> සිට <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> හා තව <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>ක්"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"වමට ගෙන යන්න"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"දකුණට ගෙන යන්න"</string>
+    <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 3eca787..638e88a 100644
--- a/quickstep/res/values-sk/strings.xml
+++ b/quickstep/res/values-sk/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Vymazať všetko"</string>
@@ -47,7 +49,7 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Predpovedaná aplikácia: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Otočte zariadenie"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Otočte zariadenie a dokončite tak návod, ako navigovať gestami"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Musíte potiahnuť úplne z pravého alebo ľavého okraja"</string>
+    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Musíte potiahnuť úplne z pravého alebo ľavého okraja."</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Musíte potiahnuť z pravého alebo ľavého okraja do stredu obrazovky a potom uvoľniť"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Naučili ste sa prejsť späť potiahnutím sprava. V ďalšom kroku sa naučíte prepínať aplikácie."</string>
     <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Dokončili ste gesto na prechod späť. V ďalšom kroku sa naučíte, ako prepínať aplikácie."</string>
@@ -58,8 +60,8 @@
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"Na poslednú obrazovku prejdete potiahnutím z ľavého alebo pravého okraja do stredu obrazovky."</string>
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Na poslednú obrazovku sa vrátite potiahnutím dvoma prstami z ľavého alebo pravého okraja do stredu obrazovky."</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Prechod späť"</string>
-    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Potiahnite z ľavého alebo pravého okraja do stredu obrazovky"</string>
-    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Musíte potiahnuť nahor z dolného okraja obrazovky"</string>
+    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Potiahnite z ľavého alebo pravého okraja do stredu obrazovky."</string>
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Musíte potiahnuť nahor z dolného okraja obrazovky."</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Pred uvoľnením nesmiete zastať"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Musíte potiahnuť priamo nahor"</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Dokončili ste gesto prechodu na plochu. Teraz sa naučíte, ako sa vrátiť späť."</string>
@@ -68,18 +70,18 @@
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Potiahnite nahor zdola obrazovky. Týmto gestom sa vždy vrátite na plochu."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Postiahnite dvoma prstami z dolnej časti obrazovky. Týmto gestom sa vždy vrátite na plochu."</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Prechod na plochu"</string>
-    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Potiahnite z dolnej časti obrazovky nahor"</string>
+    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Potiahnite z dolnej časti obrazovky nahor."</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Skvelé!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Musíte potiahnuť nahor z dolného okraja obrazovky"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Skúste okno pred uvoľnením podržať dlhšie"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Musite potiahnuť priamo nahor a potom zastať"</string>
-    <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Naučili ste sa používať gestá. Gestá môžete vypnúť v nastaveniach."</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Musite potiahnuť priamo nahor a potom zastať."</string>
+    <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Naučili ste sa používať gestá. Gestá môžete vypnúť v Nastaveniach."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Dokončili ste gesto na prepnutie aplikácií"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Prepínanie aplikácií potiahnutím"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Aplikácie môžete prepínať potiahnutím obrazovky zdola nahor, pridržaním a následným uvoľnením."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Aplikácie prepnete potiahnutím dvoma prstami z dolnej časti obrazovky, ich pridržaním a uvoľnením."</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Prepnutie aplikácií"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Potiahnite nahor z dolného okraja obrazovky, pridržte a uvoľnite"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Potiahnite nahor z dolného okraja obrazovky, pridržte a uvoľnite."</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Výborne"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Hotovo"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Hotovo"</string>
@@ -90,7 +92,7 @@
     <string name="allset_title" msgid="5021126669778966707">"Hotovo"</string>
     <string name="allset_hint" msgid="459504134589971527">"Potiahnutím nahor prejdete na plochu"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Na plochu prejdete klepnutím na tlačidlo plochy"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> môžete začať používať"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"Môžete <xliff:g id="DEVICE">%1$s</xliff:g> začať používať"</string>
     <string name="default_device_name" msgid="6660656727127422487">"zariadenie"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Nastavenia navigácie systémom"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Zdieľať"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Uložiť pár aplikácií"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Obrazovku rozdelíte klepnutím na inú aplikáciu"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Na použitie rozdelenej obrazovky vyberte ďalšiu aplikáciu"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Zrušiť"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Zrušiť"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ukončite výber rozdelenej obrazovky"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Na použitie rozd. obrazovky vyberte inú aplikáciu"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikácia alebo vaša organizácia túto akciu nepovoľuje"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Rýchle nastavenia"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Panel aplikácií"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Panel aplikácií je zobrazený"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Panel aplik. a bubl. sú vľavo"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Panel aplik. a bubl. sú vpravo"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Panel aplikácií je skrytý"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Panel aplik. a bubl. sú skryté"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigačný panel"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Zobrazovať panel aplikácií"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Zmeniť režim navigácie"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Rozdeľovač panela aplikácií"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Rozšírená ponuka panela aplikácií"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Presunúť hore alebo doľava"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Presunúť dole alebo doprava"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Zobraziť # ďalšiu aplikáciu.}few{Zobraziť # ďalšie aplikácie.}many{Show # more apps.}other{Zobraziť # ďalších aplikácií.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Zobraziť # aplikáciu pre počítač.}few{Zobraziť # aplikácie pre počítač.}many{Show # desktop apps.}other{Zobraziť # aplikácií pre počítač.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ďalšia aplikácia}few{ďalšie aplikácie}many{ďalšie aplikácie}other{ďalšie aplikácie}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Počítač"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> a <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bublina"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Rozbaľovacia ponuka"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikácie <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> a ešte <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Posunúť doľava"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Posunúť doprava"</string>
+    <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 52faeb7..30d2c03 100644
--- a/quickstep/res/values-sl/strings.xml
+++ b/quickstep/res/values-sl/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Počisti vse"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Shrani par aplikacij"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Za razdeljeni zaslon se dotaknite še 1 aplikacije"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Izberite drugo aplikacijo za uporabo razdeljenega zaslona."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Prekliči"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Prekliči"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Zapri izbiro razdeljenega zaslona"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Izberite drugo aplikacijo za uporabo razdeljenega zaslona."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ali vaša organizacija ne dovoljuje tega dejanja"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Hitre nastavitve"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Opravilna vrstica"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Opravilna vrstica je prikazana"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Prikazani so opravilna vrstica in oblački na levi"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Prikazani so opravilna vrstica in oblački na desni"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Opravilna vrstica je skrita"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Opravilna vrstica in oblački so skriti"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Vrstica za krmarjenje"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Stalen prikaz oprav. vrstice"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Spreminjanje načina navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdelilnik opravilne vrstice"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Oblaček opravilne vrstice z dodatnimi elementi"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premakni na vrh/levo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premakni na dno/desno"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Pokaži še # aplikacijo.}one{Pokaži še # aplikacijo.}two{Pokaži še # aplikaciji.}few{Pokaži še # aplikacije.}other{Pokaži še # aplikacij.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Prikaz # aplikacije za namizni računalnik.}one{Prikaz # aplikacije za namizni računalnik.}two{Prikaz # aplikacij za namizni računalnik.}few{Prikaz # aplikacij za namizni računalnik.}other{Prikaz # aplikacij za namizni računalnik.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}two{dodatni aplikaciji}few{dodatne aplikacije}other{dodatnih aplikacij}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Namizni računalnik"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> in <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Oblaček"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Oblaček z dodatnimi elementi"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> in še <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Premik v levo"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Premik v desno"</string>
+    <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 cdb9cf9..b4b6711 100644
--- a/quickstep/res/values-sq/strings.xml
+++ b/quickstep/res/values-sq/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Pastroji të gjitha"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Ruaj çiftin e aplikacioneve"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Trokit një apl. tjetër; përdor ekranin e ndarë"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Anulo"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Anulo"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Dil nga zgjedhja e ekranit të ndarë"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Ky veprim nuk lejohet nga aplikacioni ose organizata jote"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Cilësimet shpejt"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Shiriti i detyrave"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Shiriti i detyrave i shfaqur"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Shiriti i detyrave dhe flluskat majtas janë shfaqur"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Shiriti i detyrave dhe flluskat djathtas janë shfaqur"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Shiriti i detyrave i fshehur"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Shiriti i detyrave dhe flluskat janë fshehur"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Shiriti i navigimit"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Shfaq gjithmonë shiritin e detyrave"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Ndrysho modalitetin e navigimit"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Ndarësi i shiritit të detyrave"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tejkalimi i shiritit të detyrave"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Lëviz në krye/majtas"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Lëviz në fund/djathtas"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Shfaq # aplikacion tjetër.}other{Shfaq # aplikacione të tjera.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Shfaq # aplikacion për desktop.}other{Shfaq # aplikacione për desktop.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikacion tjetër}other{aplikacione të tjera}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> dhe <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Flluska"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Tejkalimi"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"\"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" nga <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"\"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>\" dhe <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> të tjera"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Lëviz majtas"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Lëviz djathtas"</string>
+    <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 7456a36..6622217 100644
--- a/quickstep/res/values-sr/strings.xml
+++ b/quickstep/res/values-sr/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Обриши све"</string>
@@ -89,7 +91,7 @@
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"Водич <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"Готово!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Превуците нагоре да бисте отворили почетни екран"</string>
-    <string name="allset_button_hint" msgid="2395219947744706291">"Додирните дугме Почетак да бисти ишли на почетни екран"</string>
+    <string name="allset_button_hint" msgid="2395219947744706291">"Додирните дугме Почетак да бисте отишли на почетни екран"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Спремни сте да почнете да користите <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"уређај"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Подешавања кретања кроз систем"</annotation></string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Сачувај пар апликација"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Додирните другу апликацију за подељени екран"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Одаберите другу апликацију да бисте користили подељени екран"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Откажи"</b></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="blocked_by_policy" msgid="2071401072261365546">"Апликација или организација не дозвољавају ову радњу"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Брза подешавања"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Трака задатака"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Трака задатака је приказана"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Приказ задатака/облачића лево"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Приказ задатака/облачића десно"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Трака задатака је скривена"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Скривени задаци/облачићи"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Трака за навигацију"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Увек приказуј траку задатака"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Промени режим навигације"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделник траке задатака"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Преклопна трака задатака"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Премести горе лево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести доле десно"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Прикажи још # апликацију.}one{Прикажи још # апликацију.}few{Прикажи још # апликације.}other{Прикажи још # апликација.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Прикажи # апликацију за рачунаре.}one{Прикажи # апликацију за рачунаре.}few{Прикажи # апликације за рачунаре.}other{Прикажи # апликација за рачунаре.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{додатна апликација}one{додатна апликација}few{додатне апликације}other{додатних апликација}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Рачунар"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Облачић"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Преклопни"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и још <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Помери налево"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Помери надесно"</string>
+    <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 f369dae..089d1b5 100644
--- a/quickstep/res/values-sv/strings.xml
+++ b/quickstep/res/values-sv/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Rensa alla"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Spara app-par"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tryck på en annan app för att använda delad skärm"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Välj en annan app för att använda delad skärm"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Avbryt"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Avbryt"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Avsluta val av delad skärm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Välj en annan app för att använda delad skärm"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller organisationen tillåter inte den här åtgärden"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Snabbinställn."</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Aktivitetsfält"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Aktivitetsfältet visas"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Vänster fält och bubblor visas"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Höger fält och bubblor visas"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Aktivitetsfältet är dolt"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Fält och bubblor dolda"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigeringsfält"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Visa alltid aktivitetsfältet"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Ändra navigeringsläge"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Avdelare för aktivitetsfältet"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Fler alternativ för aktivitetsfältet"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flytta högst upp/till vänster"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytta längst ned/till höger"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Visa # app till.}other{Visa # appar till.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Visa # datorapp.}other{Visa # datorappar.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app till}other{appar till}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Dator"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> och <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubbla"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Fler alternativ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> från <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> och <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> till"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Flytta åt vänster"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Flytta åt höger"</string>
+    <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 3d8277b..821797b 100644
--- a/quickstep/res/values-sw/strings.xml
+++ b/quickstep/res/values-sw/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Ondoa zote"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Hifadhi jozi ya programu"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Gusa programu nyingine ili utumie kipengele cha kugawa skrini"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Chagua programu nyingine ili utumie hali ya kugawa skrini"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Ghairi"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Acha"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ondoka kwenye hali ya skrini iliyogawanywa"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Chagua programu nyingine ili utumie hali ya kugawa skrini"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Kitendo hiki hakiruhusiwi na programu au shirika lako"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Mipangilio ya Haraka"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Upauzana"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Upauzana umeonyeshwa"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Upauzana na viputo vinaonyeshwa kushoto"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Upauzana na viputo vinaonyeshwa kulia"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Upauzana umefichwa"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Upauzana na viputo vimefichwa"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Sehemu ya viungo muhimu"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Onyesha Zana kila wakati"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Badilisha hali ya usogezaji"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Kitenganishi cha Upauzana"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Upauzana wa Vipengele vya Ziada"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sogeza juu/kushoto"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sogeza chini/kulia"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Onyesha programu # zaidi.}other{Onyesha programu # zaidi.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Onyesha programu # ya kompyuta ya mezani.}other{Onyesha programu # za kompyuta ya mezani.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{programu nyingine}other{programu zingine}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Kompyuta ya Mezani"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> na <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Kiputo"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Kiputo cha vipengee vya ziada"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kutoka <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> na vingine <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Sogeza kushoto"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Sogeza kulia"</string>
+    <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 ed3ebee..7bbfaba 100644
--- a/quickstep/res/values-ta/strings.xml
+++ b/quickstep/res/values-ta/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"எல்லாம் அழி"</string>
@@ -72,7 +74,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"அருமை!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"திரையின் கீழ் ஓரத்திலிருந்து மேல்நோக்கி ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்"</string>
     <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_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_intro_title" msgid="2902054412868489378">"ஆப்ஸுக்கிடையே மாற ஸ்வைப் செய்யுங்கள்"</string>
@@ -90,7 +92,7 @@
     <string name="allset_title" msgid="5021126669778966707">"அனைத்தையும் அமைத்துவிட்டீர்கள்!"</string>
     <string name="allset_hint" msgid="459504134589971527">"முகப்புக்குச் செல்ல மேல்நோக்கி ஸ்வைப் செய்யுங்கள்"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"முகப்புத் திரைக்குச் செல்வதற்கு முகப்பு பட்டனைத் தட்டவும்"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"உங்கள் <xliff:g id="DEVICE">%1$s</xliff:g> சாதனத்தைப் பயன்படுத்தத் தயாராகிவிட்டீர்கள்"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"உங்கள் <xliff:g id="DEVICE">%1$s</xliff:g> ஐப் பயன்படுத்தத் தயாராகிவிட்டீர்கள்"</string>
     <string name="default_device_name" msgid="6660656727127422487">"சாதனம்"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"சிஸ்டம் வழிசெலுத்தல் அமைப்புகள்"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"பகிர்"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ஆப்ஸ் ஜோடியைச் சேமி"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தட்டவும்"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தேர்வுசெய்யுங்கள்"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ரத்துசெய்"</b></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="blocked_by_policy" msgid="2071401072261365546">"ஆப்ஸோ உங்கள் நிறுவனமோ இந்த செயலை அனுமதிப்பதில்லை"</string>
@@ -122,7 +124,7 @@
     <string name="taskbar_edu_close" msgid="887022990168191073">"மூடுக"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"முடிந்தது"</string>
     <string name="taskbar_button_home" msgid="2151398979630664652">"முகப்பு"</string>
-    <string name="taskbar_button_a11y" msgid="5241161324875094465">"அணுகல்தன்மை"</string>
+    <string name="taskbar_button_a11y" msgid="5241161324875094465">"மாற்றுத்திறன் வசதி"</string>
     <string name="taskbar_button_back" msgid="8558862226461164514">"பின்செல்லும்"</string>
     <string name="taskbar_button_ime_switcher" msgid="1730244360907588541">"IME சுவிட்ச்சர்"</string>
     <string name="taskbar_button_recents" msgid="7273376136216613134">"சமீபத்தியவை"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"விரைவு அமைப்புகள்"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"செயல் பட்டி"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"செயல் பட்டி காட்டப்படுகிறது"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"செயல் பட்டி &amp; குமிழை இடதுபுறம் காட்டும்"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"செயல் பட்டி &amp; குமிழை வலதுபுறம் காட்டும்"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"செயல் பட்டி மறைக்கப்பட்டுள்ளது"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"செயல் பட்டி &amp; குமிழை மறைக்கும்"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"வழிசெலுத்தல் பட்டி"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"செயல் பட்டியை எப்போதும் காட்டு"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"வழிசெலுத்தல் பயன்முறையை மாற்று"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"செயல் பட்டிப் பிரிப்பான்"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"செயல் பட்டிக்கான கூடுதல் விருப்பங்கள்"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"மேலே/இடதுபுறம் நகர்த்தும்"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"கீழே/வலதுபுறம் நகர்த்தும்"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{மேலும் # ஆப்ஸைக் காட்டு.}other{மேலும் # ஆப்ஸைக் காட்டு.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}other{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{கூடுதல் ஆப்ஸ்}other{கூடுதல் ஆப்ஸ்}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"டெஸ்க்டாப்"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> மற்றும் <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"குமிழ்"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"கூடுதல் விருப்பங்களைக் காட்டும்"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> வழங்கும் <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> மற்றும் <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"இடதுபுறம் நகர்த்தும்"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"வலதுபுறம் நகர்த்தும்"</string>
+    <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 a4e1cbf..5439e80 100644
--- a/quickstep/res/values-te/strings.xml
+++ b/quickstep/res/values-te/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"అన్నీ తీసివేయండి"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"యాప్ పెయిర్‌ను సేవ్ చేయండి"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్‌ను ట్యాప్ చేయండి"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"స్ప్లిట్ స్క్రీన్‌ను ఉపయోగించడానికి మరొక యాప్ ఎంచుకోండి"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"రద్దు చేయండి"</b></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="blocked_by_policy" msgid="2071401072261365546">"ఈ చర్యను యాప్ గానీ, మీ సంస్థ గానీ అనుమతించవు"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"క్విక్ సెట్టింగ్‌లు"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"టాస్క్‌బార్"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"టాస్క్‌బార్ చూపబడింది"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"టాస్క్‌బార్, బబుల్స్ ఎడమవైపున చూపబడ్డాయి"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"టాస్క్‌బార్, బబుల్స్ కుడివైపున చూపబడ్డాయి"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"టాస్క్‌బార్ దాచబడింది"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"టాస్క్‌బార్, బబుల్స్ దాచబడినవి"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"నావిగేషన్ బార్"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"టాస్క్‌బార్‌ను నిరంతరం చూపండి"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"నావిగేషన్ మోడ్‌ను మార్చండి"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"టాస్క్‌బార్ డివైడర్"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"టాస్క్‌బార్ ఓవర్‌ఫ్లో"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ఎగువ/ఎడమ వైపునకు తరలించండి"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"దిగువ/కుడి వైపునకు తరలించండి"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{మరో # యాప్‌ను చూడండి.}other{మరో # యాప్‌లను చూడండి.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# డెస్క్‌టాప్ యాప్‌ను చూపండి.}other{# డెస్క్‌టాప్ యాప్‌లను చూపండి.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{మరో యాప్‌}other{మరిన్ని యాప్‌లు}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"డెస్క్‌టాప్"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g>, <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"బబుల్"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ఓవర్‌ఫ్లో"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> నుండి <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>, మరో <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ఎడమ వైపుగా జరపండి"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"కుడి వైపుగా జరపండి"</string>
+    <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 1bbb137..30e73e6 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"ล้างทั้งหมด"</string>
@@ -47,11 +49,11 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"แอปที่คาดว่าจะใช้: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"หมุนอุปกรณ์ของคุณ"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"โปรดหมุนอุปกรณ์เพื่อทำตามบทแนะนำการนำทางด้วยท่าทางสัมผัสให้เสร็จสมบูรณ์"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"ตรวจสอบว่าปัดจากขอบด้านขวาสุดหรือซ้ายสุด"</string>
+    <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>
@@ -62,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>
@@ -72,9 +74,9 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"เก่งมาก"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"ปัดขึ้นจากขอบด้านล่างของหน้าจอ"</string>
     <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_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>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"บันทึกคู่แอป"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"แตะแอปอื่นเพื่อใช้การแยกหน้าจอ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"ยกเลิก"</b></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="blocked_by_policy" msgid="2071401072261365546">"แอปหรือองค์กรของคุณไม่อนุญาตการดำเนินการนี้"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"การตั้งค่าด่วน"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"แถบงาน"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"แถบงานแสดงอยู่"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"แถบงานและบับเบิลแสดงไว้ทางซ้าย"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"แถบงานและบับเบิลแสดงไว้ทางขวา"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"แถบงานซ่อนอยู่"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"แถบงานและบับเบิลซ่อนอยู่"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"แถบนำทาง"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"แสดงแถบงานเสมอ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"เปลี่ยนโหมดการนําทาง"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ตัวแบ่งแถบงาน"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"การดำเนินการเพิ่มเติมของแถบงาน"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ย้ายไปที่ด้านบนหรือด้านซ้าย"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ย้ายไปที่ด้านล่างหรือด้านขวา"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{แสดงเพิ่มเติมอีก # แอป}other{แสดงเพิ่มเติมอีก # แอป}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{แสดงแอปบนเดสก์ท็อป # รายการ}other{แสดงแอปบนเดสก์ท็อป # รายการ}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{แอปเพิ่มเติม}other{แอปเพิ่มเติม}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"เดสก์ท็อป"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> และ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"บับเบิล"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"การดำเนินการเพิ่มเติม"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> จาก <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> และอีก <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> รายการ"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"ย้ายไปทางซ้าย"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"ย้ายไปทางขวา"</string>
+    <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 978a5a3..583f419 100644
--- a/quickstep/res/values-tl/strings.xml
+++ b/quickstep/res/values-tl/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"I-clear lahat"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"I-save ang app pair"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Mag-tap ng ibang app para gamitin ang split screen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Pumili ng ibang app para gamitin ang split screen"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Kanselahin"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Kanselahin"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Lumabas sa pagpili ng split screen"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pumili ng ibang app para gamitin ang split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Hindi pinapayagan ng app o ng iyong organisasyon ang pagkilos na ito"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Quick Settings"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Taskbar"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Ipinapakita ang taskbar"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Taskbar at bubble sa kaliwa"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Taskbar at bubble sa kanan"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Nakatago ang taskbar"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Nakatago ang taskbar at bubble"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigation bar"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ipakita lagi ang Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Magpalit ng navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divider ng Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar Overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Ilipat sa itaas/kaliwa"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Ilipat sa ibaba/kanan"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Magpakita ng # pang app.}one{Magpakita ng # pang app.}other{Magpakita ng # pang app.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Ipakita ang # desktop app.}one{Ipakita ang # desktop app.}other{Ipakita ang # na desktop app.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{pang app}one{pang app}other{pang app}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Desktop"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> at <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bubble"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> mula sa <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> at <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> pa"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Ilipat pakaliwa"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Ilipat pakanan"</string>
+    <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 0cc5d7f..c50c1f8 100644
--- a/quickstep/res/values-tr/strings.xml
+++ b/quickstep/res/values-tr/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Tümünü temizle"</string>
@@ -90,7 +92,7 @@
     <string name="allset_title" msgid="5021126669778966707">"İşlem tamam!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Ana ekrana gitmek için yukarı kaydırın"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Ana ekranınıza gitmek için ana sayfa düğmesine dokunun"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> cihazınızı kullanmaya hazırsınız"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> adlı cihazınızı kullanmaya hazırsınız"</string>
     <string name="default_device_name" msgid="6660656727127422487">"cihaz"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Sistem gezinme ayarları"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Paylaş"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Uygulama çiftini kaydet"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Bölünmüş ekran için başka bir uygulamaya dokunun"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Bölünmüş ekran kullanmak için başka bir uygulama seçin"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"İptal"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"İptal"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Bölünmüş ekran seçiminden çıkın"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Bölünmüş ekran kullanmak için başka bir uygulama seçin"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Uygulamanız veya kuruluşunuz bu işleme izin vermiyor"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Hızlı Ayarlar"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Görev çubuğu."</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Görev çubuğu gösteriliyor"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Görev çubuğu ve baloncuklar solda gösteriliyor"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Görev çubuğu ve baloncuklar sağda gösteriliyor"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Görev çubuğu gizlendi"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Görev çubuğu ve baloncuklar gizli"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Gezinme çubuğu"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Görev çubuğunu daima göster"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Gezinme modunu değiştir"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Görev Çubuğu Ayırıcısı"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Görev Çubuğu Taşması"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sol üste taşı"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sağ alta taşı"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# uygulama daha göster.}other{# uygulama daha göster}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# masaüstü uygulamasını göster.}other{# masaüstü uygulamasını göster.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{uygulama daha}other{uygulama daha}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Masaüstü"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ve <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Balon"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Taşma"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> uygulamasından <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ve <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> tane daha"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Sola taşı"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Sağa taşı"</string>
+    <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 9c706a8..d75f8b4 100644
--- a/quickstep/res/values-uk/strings.xml
+++ b/quickstep/res/values-uk/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Очистити все"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Зберегти пару"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Щоб розділити екран, виберіть ще один додаток."</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Щоб розділити екран, виберіть ще один додаток."</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Скасувати"</b></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="blocked_by_policy" msgid="2071401072261365546">"Ця дія заборонена додатком або адміністратором організації"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Швидкі налаштув."</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Панель завдань"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Панель завдань показано"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Панель завдань і чати – зліва"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Панель завдань і чати – справа"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Панель завдань приховано"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Панель і чати приховано"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Панель навігації"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Завжди показув. панель завдань"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Змінити режим навігації"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Розділювач панелі завдань"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Додаткове меню панелі завдань"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Перемістити вгору або вліво"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перемістити вниз або вправо"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Показати ще # додаток.}one{Показати ще # додаток.}few{Показати ще # додатки.}many{Показати ще # додатків.}other{Показати ще # додатка.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Показати # комп’ютерну програму.}one{Показати # комп’ютерну програму.}few{Показати # комп’ютерні програми.}many{Показати # комп’ютерних програм.}other{Показати # комп’ютерної програми.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{інший додаток}one{інший додаток}few{інші додатки}many{інших додатків}other{іншого додатка}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Комп’ютер"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> та <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Повідомлення"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Додаткове повідомлення"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> з додатка <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> і ще <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Перемістити вліво"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Перемістити вправо"</string>
+    <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 e125248..68f838f 100644
--- a/quickstep/res/values-ur/strings.xml
+++ b/quickstep/res/values-ur/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"سبھی کو صاف کریں"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"ایپس کے جوڑے کو محفوظ کریں"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"اسپلٹ اسکرین کا استعمال کرنے کیلئے دوسری ایپ پر تھپتھپائیں"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"منسوخ کریں"</b></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="blocked_by_policy" msgid="2071401072261365546">"ایپ یا آپ کی تنظیم کی جانب سے اس کارروائی کی اجازت نہیں ہے"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"فوری ترتیبات"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"ٹاسک بار"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"ٹاشک بار دکھایا گیا"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ٹاسک بار و بلبلے بائیں طرف ہیں"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ٹاسک بار و بلبلے دائیں طرف ہیں"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"ٹاسک بار چھپایا گیا"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ٹاسک بار اور بلبلے پوشیدہ ہیں"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"نیویگیشن بار"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"ہمیشہ ٹاسک بار دکھائیں"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"نیویگیشن موڈ تبدیل کریں"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ٹاسک بار ڈیوائیڈر"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ٹاسک بار اوورفلو"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"اوپر/بائیں طرف منتقل کریں"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"نیچے/دائیں طرف منتقل کریں"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# مزید ایپ دکھائیں۔}other{# مزید ایپس دکھائیں۔}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ڈیسک ٹاپ ایپ دکھائیں۔}other{# ڈیسک ٹاپ ایپس دکھائیں۔}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{مزید ایپ}other{مزید ایپس}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"ڈیسک ٹاپ"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> اور <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"ببل"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"اوورفلو"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> سے <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> اور <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> مزید"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"بائیں منتقل کریں"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"دائیں منتقل کریں"</string>
+    <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 3f4f981..294be84 100644
--- a/quickstep/res/values-uz/strings.xml
+++ b/quickstep/res/values-uz/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Hammasini tozalash"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Ilova juftini saqlash"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ekranni ikkiga ajratish uchun boshqa ilovani bosing"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Ekranni ikkiga ajratish uchun boshqa ilovani tanlang"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Bekor qilish"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Bekor qilish"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ekranni ikkiga ajratish tanlovidan chiqish"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Ekranni ikkiga ajratish uchun boshqa ilovani tanlang"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Bu amal ilova yoki tashkilotingiz tomonidan taqiqlangan"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Tezkor sozlamalar"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Vazifalar paneli"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Vazifalar paneli ochiq"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Panel va bulutchalar chapda"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Panel va bulutchalar oʻngda"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Vazifalar paneli yopiq"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Panel va bulutchalar berk"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Navigatsiya paneli"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vazifalar paneli doim chiqarilsin"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigatsiya rejimini oʻzgartirish"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Vazifalar panelini ajratkich"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Vazifalar panelini kengaytirish"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Yuqoriga yoki chapga oʻtkazish"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pastga yoki oʻngga oʻtkazish"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Yana # ta ilovani chiqarish}other{Yana # ta ilovani chiqarish}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{# ta desktop ilovani chiqarish.}other{# ta desktop ilovani chiqarish.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{boshqa ilova}other{boshqa ilovalar}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Kompyuter"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> va <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Pufak"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Kengaytirish"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> va yana <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> kishi"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Chapga siljitish"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Oʻngga siljitish"</string>
+    <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 9bc526f..7eeacde 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Xóa tất cả"</string>
@@ -51,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>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Lưu cặp ứng dụng"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Nhấn vào ứng dụng khác để chia đôi màn hình"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Huỷ"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Huỷ"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Thoát khỏi lựa chọn chia đôi màn hình"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Ứng dụng hoặc tổ chức của bạn không cho phép thực hiện hành động này"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Cài đặt nhanh"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"Thanh tác vụ"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Đã hiện thanh thao tác"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"Hiện thanh tác vụ, b.bóng trái"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"Hiện thanh tác vụ, b.bóng phải"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Đã ẩn thanh thao tác"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"Đã ẩn thanh tác vụ &amp; bong bóng"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Thanh điều hướng"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Luôn hiện Thanh tác vụ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Thay đổi chế độ điều hướng"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Đường phân chia Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Trình đơn mục bổ sung trên thanh tác vụ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Chuyển lên trên cùng/sang bên trái"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Chuyển xuống dưới cùng/sang bên phải"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Hiện thêm # ứng dụng.}other{Hiện thêm # ứng dụng.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Hiện # ứng dụng dành cho máy tính.}other{Hiện # ứng dụng dành cho máy tính.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ứng dụng khác}other{ứng dụng khác}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Máy tính"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> và <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bong bóng"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Bong bóng bổ sung"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> từ <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> và <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> bong bóng khác"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Di chuyển sang trái"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Di chuyển sang phải"</string>
+    <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 79ea299..dc16036 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"全部清除"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"保存应用组合"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"点按另一个应用即可使用分屏"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"另外选择一个应用才可使用分屏模式"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"取消"</b></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="blocked_by_policy" msgid="2071401072261365546">"该应用或您所在的单位不允许执行此操作"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"快捷设置"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"任务栏"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"任务栏已显示"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"已显示任务栏和左侧消息气泡"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"已显示任务栏和右侧消息气泡"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"任务栏已隐藏"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"已隐藏任务栏和消息气泡"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"导航栏"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"始终显示任务栏"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"更改导航模式"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"任务栏分隔线"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"任务栏溢出图标"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移到顶部/左侧"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右侧"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{显示另外 # 个应用。}other{显示另外 # 个应用。}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{显示 # 款桌面应用。}other{显示 # 款桌面应用。}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{多个应用}other{多个应用}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"桌面模式"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g>和<xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"气泡框"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"溢出式气泡框"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"来自“<xliff:g id="APP_NAME">%2$s</xliff:g>”的<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>以及另外 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 个"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"左移"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"右移"</string>
+    <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 b9d8eb7..c8d18eb 100644
--- a/quickstep/res/values-zh-rHK/strings.xml
+++ b/quickstep/res/values-zh-rHK/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"全部清除"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"儲存應用程式組合"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"輕按其他應用程式以使用分割螢幕"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"選擇其他應用程式才能使用分割螢幕"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"取消"</b></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="blocked_by_policy" msgid="2071401072261365546">"應用程式或你的機構不允許此操作"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"快速設定"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"工作列"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"顯示咗工作列"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"工作列和對話氣泡在左邊顯示"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"工作列和對話氣泡在右邊顯示"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"隱藏咗工作列"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"工作列和對話氣泡已隱藏"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"導覽列"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"一律顯示工作列"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"變更導覽模式"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"工作列分隔線"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"工作列溢位"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移至上方/左側"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移至底部/右側"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{顯示另外 # 個應用程式。}other{顯示另外 # 個應用程式。}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{顯示 # 個桌面應用程式。}other{顯示 # 個桌面應用程式。}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個其他應用程式}other{個其他應用程式}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"桌面"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"「<xliff:g id="APP_NAME_1">%1$s</xliff:g>」和「<xliff:g id="APP_NAME_2">%2$s</xliff:g>」"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"對話氣泡"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"展開式"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> 的「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」通知"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>和其他 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 則通知"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"向左移"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"向右移"</string>
+    <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 90140cb..fd132d2 100644
--- a/quickstep/res/values-zh-rTW/strings.xml
+++ b/quickstep/res/values-zh-rTW/strings.xml
@@ -21,7 +21,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <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_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>
     <string name="recents_clear_all" msgid="5328176793634888831">"全部清除"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"儲存應用程式配對"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"輕觸另一個應用程式即可使用分割畫面"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"選擇要在分割畫面中使用的另一個應用程式"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"取消"</b></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="blocked_by_policy" msgid="2071401072261365546">"這個應用程式或貴機構不允許執行這個動作"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"快速設定"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"工作列"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"已顯示工作列"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"工作列和對話框顯示在左側"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"工作列和對話框顯示在右側"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"已隱藏工作列"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"已隱藏工作列和對話框"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"導覽列"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"一律顯示工作列"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"變更操作模式"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"工作列分隔線"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"工作列溢位"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移到上方/左側"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右側"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{再多顯示 # 個應用程式。}other{再多顯示 # 個應用程式。}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{顯示 # 個電腦版應用程式。}other{顯示 # 個電腦版應用程式。}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個其他應用程式}other{個其他應用程式}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"電腦"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"「<xliff:g id="APP_NAME_1">%1$s</xliff:g>」和「<xliff:g id="APP_NAME_2">%2$s</xliff:g>」"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"泡泡"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"溢位"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"「<xliff:g id="APP_NAME">%2$s</xliff:g>」的「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」通知"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>和另外 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 則通知"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"向左移"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"向右移"</string>
+    <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 73be445..46dbbd5 100644
--- a/quickstep/res/values-zu/strings.xml
+++ b/quickstep/res/values-zu/strings.xml
@@ -22,6 +22,8 @@
     <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>
     <string name="recents_clear_all" msgid="5328176793634888831">"Sula konke"</string>
@@ -99,7 +101,7 @@
     <string name="action_save_app_pair" msgid="5974823919237645229">"Londoloza ukubhangqa i-app"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Thepha enye i-app ukuze usebenzise isikrini sokuhlukanisa"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini"</string>
-    <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Khansela"</b></string>
+    <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Khansela"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Phuma ekukhetheni ukuhlukaniswa kwesikrini"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Lesi senzo asivunyelwanga uhlelo lokusebenza noma inhlangano yakho"</string>
@@ -130,18 +132,28 @@
     <string name="taskbar_button_quick_settings" msgid="227662894293189391">"Amasethingi Asheshayo"</string>
     <string name="taskbar_a11y_title" msgid="6432169809852243110">"I-Taskbar"</string>
     <string name="taskbar_a11y_shown_title" msgid="6842833581088937713">"Ibha yomsebenzi ibonisiwe"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_left_title" msgid="4242431789851790046">"ITaskbar namabhamuza aboniswe kwesokunxele"</string>
+    <string name="taskbar_a11y_shown_with_bubbles_right_title" msgid="8219065376188180113">"ITaskbar namabhamuza aboniswe kwesokudla"</string>
     <string name="taskbar_a11y_hidden_title" msgid="9154903639589659284">"Ibha yomsebenzi ifihliwe"</string>
+    <string name="taskbar_a11y_hidden_with_bubbles_title" msgid="7397395993149508087">"ITaskbar namabhamuza afihliwe"</string>
     <string name="taskbar_phone_a11y_title" msgid="4933360237131229395">"Ibha yokufuna"</string>
     <string name="always_show_taskbar" msgid="3608801276107751229">"Bonisa i-Taskbar njalo."</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Shintsha imodi yokufuna"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Isihlukanisi se-Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Ukuphuphuma Kwetaskbar"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Hamba phezulu/kwesokunxele"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Hamba phansi/kwesokudla"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Bonisa i-app e-# ngaphezulu.}one{Bonisa ama-app angu-# ngaphezulu.}other{Bonisa ama-app angu-# ngaphezulu.}}"</string>
-    <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Bonisa i-app engu-# yedeskithophu.}one{Bonisa ama-app angu-# wedeskithophu.}other{Bonisa ama-app angu-# wedeskithophu.}}"</string>
+    <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{i-app eyengeziwe}one{ama-app engeziwe}other{ama-app engeziwe}}"</string>
+    <string name="quick_switch_desktop" msgid="8393802056024499749">"Ideskithophu"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"I-<xliff:g id="APP_NAME_1">%1$s</xliff:g> ne-<xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Ibhamuza"</string>
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ukugcwala kakhulu"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kusuka ku-<xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> nokunye okungu-<xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <string name="bubble_bar_action_move_left" msgid="3306922475737714758">"Iya kwesokunxele"</string>
+    <string name="bubble_bar_action_move_right" msgid="3455099638571411251">"Iya kwesokudla"</string>
+    <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/attrs.xml b/quickstep/res/values/attrs.xml
index ccc7f18..7fd6b5c 100644
--- a/quickstep/res/values/attrs.xml
+++ b/quickstep/res/values/attrs.xml
@@ -28,6 +28,7 @@
         <!-- Border color for a keyboard quick switch task views -->
         <attr name="focusBorderColor" format="color" />
         <attr name="hoverBorderColor" format="color" />
+        <attr name="focusBorderRadius" format="dimension" />
     </declare-styleable>
 
     <declare-styleable name="ClearAllButton">
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 0f997f9..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>
@@ -94,7 +93,5 @@
     <color name="lottie_yellow600">#f9ab00</color>
 
     <!-- Turn on work apps button -->
-    <color name="work_turn_on_stroke">?androidprv:attr/colorAccentPrimaryVariant</color>
-    <color name="work_fab_bg_color">?androidprv:attr/materialColorPrimaryFixedDim</color>
-    <color name="work_fab_icon_color">?androidprv:attr/materialColorOnPrimaryFixed</color>
+    <color name="work_turn_on_stroke">?attr/materialColorPrimary</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index fd12210..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="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 d4f66e2..782a705 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -41,6 +41,8 @@
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <!--  The icon size for the focused task, placed in center of touch target  -->
     <dimen name="task_thumbnail_icon_drawable_size">44dp</dimen>
+    <!--  The splash icon size on in Overview when a task is settled in the list -->
+    <dimen name="task_thumbnail_splash_icon_size">52dp</dimen>
     <!--  The border width shown when task is hovered  -->
     <dimen name="task_hover_border_width">4dp</dimen>
     <!--  The space under the focused task icon  -->
@@ -109,6 +111,7 @@
     <dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
     <dimen name="motion_pause_detector_speed_slow">0.15dp</dimen>
     <dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
+    <dimen name="motion_pause_detector_speed_trackpad_somewhat_fast">0.7dp</dimen>
     <dimen name="motion_pause_detector_speed_fast">1.4dp</dimen>
     <dimen name="motion_pause_detector_min_displacement_from_app">36dp</dimen>
     <dimen name="quickstep_fling_threshold_speed">0.5dp</dimen>
@@ -348,19 +351,17 @@
     <dimen name="taskbar_back_button_left_margin_kids">48dp</dimen>
     <dimen name="taskbar_home_button_left_margin_kids">48dp</dimen>
     <dimen name="taskbar_icon_size_kids">32dp</dimen>
-    <dimen name="taskbar_all_apps_button_translation_x_offset">6dp</dimen>
     <dimen name="taskbar_all_apps_search_button_translation_x_offset">6dp</dimen>
     <dimen name="taskbar_contextual_button_suw_margin">64dp</dimen>
     <dimen name="taskbar_contextual_button_suw_height">64dp</dimen>
     <dimen name="taskbar_back_button_suw_start_margin">48dp</dimen>
     <dimen name="taskbar_back_button_suw_bottom_margin">1dp</dimen>
     <dimen name="taskbar_back_button_suw_height">72dp</dimen>
-    <dimen name="taskbar_running_app_indicator_height">4dp</dimen>
-    <dimen name="taskbar_running_app_indicator_width">14dp</dimen>
-    <dimen name="taskbar_running_app_indicator_top_margin">2dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_height">2dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_width">12dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_top_margin">2dp</dimen>
+    <dimen name="taskbar_running_app_indicator_height">2dp</dimen>
+    <dimen name="taskbar_running_app_indicator_width">12dp</dimen>
+    <dimen name="taskbar_running_app_indicator_top_margin">4dp</dimen>
+    <dimen name="taskbar_minimized_app_indicator_width">6dp</dimen>
+    <dimen name="taskbar_overflow_button_preview_stroke">2dp</dimen>
 
     <!-- Transient taskbar -->
     <dimen name="transient_taskbar_padding">12dp</dimen>
@@ -374,6 +375,7 @@
     <dimen name="transient_taskbar_stash_spring_velocity_dp_per_s">400dp</dimen>
     <dimen name="taskbar_tooltip_vertical_padding">8dp</dimen>
     <dimen name="taskbar_tooltip_horizontal_padding">16dp</dimen>
+    <dimen name="taskbar_tooltip_y_offset">4dp</dimen>
 
     <!-- An additional touch slop to prevent x-axis movement during the swipe up to show taskbar -->
     <dimen name="transient_taskbar_clamped_offset_bound">16dp</dimen>
@@ -422,6 +424,7 @@
     <!--- Taskbar Pinning -->
     <dimen name="taskbar_pinning_popup_menu_width">300dp</dimen>
     <dimen name="taskbar_pinning_popup_menu_vertical_margin">16dp</dimen>
+    <dimen name="taskbar_pinning_popup_menu_min_padding_from_screen_edge">16dp</dimen>
 
     <!--- Floating Ime Inset height-->
     <dimen name="floating_ime_inset_height">60dp</dimen>
@@ -434,6 +437,7 @@
     <dimen name="bubblebar_stashed_handle_width">55dp</dimen>
     <dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
     <dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
+    <dimen name="bubblebar_stashed_handle_spring_velocity_dp_per_s">@dimen/transient_taskbar_stash_spring_velocity_dp_per_s</dimen>
     <!-- this is a pointer height minus 1dp to ensure the pointer overlaps with the bubblebar
     background appropriately when close to the rounded corner -->
     <dimen name="bubblebar_pointer_visible_size">9dp</dimen>
@@ -449,11 +453,13 @@
 
     <dimen name="bubblebar_icon_size_small">32dp</dimen>
     <dimen name="bubblebar_icon_size">36dp</dimen>
+    <dimen name="bubblebar_icon_size_persistent_taskbar">28dp</dimen>
     <dimen name="bubblebar_badge_size">24dp</dimen>
     <dimen name="bubblebar_icon_overlap">12dp</dimen>
     <dimen name="bubblebar_overflow_inset">16dp</dimen>
     <dimen name="bubblebar_icon_spacing">6dp</dimen>
     <dimen name="bubblebar_icon_spacing_large">8dp</dimen>
+    <dimen name="bubblebar_icon_spacing_persistent_taskbar">@dimen/bubblebar_icon_spacing</dimen>
     <dimen name="bubblebar_expanded_icon_spacing">12dp</dimen>
     <dimen name="bubblebar_icon_elevation">1dp</dimen>
 
@@ -474,6 +480,17 @@
     <dimen name="bubble_expanded_view_drop_target_padding">24dp</dimen>
     <dimen name="bubble_expanded_view_drop_target_margin">16dp</dimen>
 
+    <!-- Bubble bar flyout view -->
+    <dimen name="bubblebar_flyout_padding">16dp</dimen>
+    <dimen name="bubblebar_flyout_elevation">4dp</dimen>
+    <dimen name="bubblebar_flyout_avatar_message_space">14dp</dimen>
+    <dimen name="bubblebar_flyout_min_width">238dp</dimen>
+    <dimen name="bubblebar_flyout_max_width">276dp</dimen>
+    <dimen name="bubblebar_flyout_triangle_width">12dp</dimen>
+    <dimen name="bubblebar_flyout_triangle_height">10dp</dimen>
+    <dimen name="bubblebar_flyout_triangle_overlap_amount">1dp</dimen>
+    <dimen name="bubblebar_flyout_triangle_radius">2dp</dimen>
+
     <!-- Launcher splash screen -->
     <!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
     <!--     starting_surface_exit_animation_window_shift_length -->
@@ -482,17 +499,23 @@
     <!-- Keyboard Quick Switch -->
     <dimen name="keyboard_quick_switch_border_width">4dp</dimen>
     <dimen name="keyboard_quick_switch_taskview_width">104dp</dimen>
-    <dimen name="keyboard_quick_switch_taskview_height">134dp</dimen>
+    <dimen name="keyboard_quick_switch_taskview_height">136dp</dimen>
     <dimen name="keyboard_quick_switch_taskview_icon_size">52dp</dimen>
     <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_split_view_spacing">2dp</dimen>
+    <dimen name="keyboard_quick_switch_view_small_spacing">4dp</dimen>
     <dimen name="keyboard_quick_switch_view_radius">28dp</dimen>
     <dimen name="keyboard_quick_switch_task_view_radius">16dp</dimen>
     <dimen name="keyboard_quick_switch_no_recent_items_icon_size">24dp</dimen>
     <dimen name="keyboard_quick_switch_no_recent_items_icon_margin">8dp</dimen>
+    <dimen name="keyboard_quick_switch_text_button_width">104dp</dimen>
+    <dimen name="keyboard_quick_switch_text_button_radius">360dp</dimen>
+    <dimen name="keyboard_quick_switch_text_button_horizontal_padding">16dp</dimen>
+    <dimen name="keyboard_quick_switch_text_button_fade_edge_length">20dp</dimen>
 
     <!-- Digital Wellbeing -->
     <dimen name="digital_wellbeing_toast_height">48dp</dimen>
diff --git a/quickstep/res/drawable/bg_circle.xml b/quickstep/res/values/ids.xml
similarity index 64%
rename from quickstep/res/drawable/bg_circle.xml
rename to quickstep/res/values/ids.xml
index 506177b..c71bb76 100644
--- a/quickstep/res/drawable/bg_circle.xml
+++ b/quickstep/res/values/ids.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2020 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.
@@ -14,7 +14,11 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="oval">
-    <solid android:color="#FFFFFFFF" />
-</shape>
\ No newline at end of file
+<resources>
+    <!-- Used for A11y actions for bubble bar -->
+    <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 340d25b..026e25c 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -26,8 +26,13 @@
     <string name="recent_task_option_pin">Pin</string>
     <!-- Title for an option to enter freeform mode for a given app -->
     <string name="recent_task_option_freeform">Freeform</string>
-    <!-- Title for an option to enter desktop windowing mode for a given app -->
+    <!-- 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>
 
     <!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
     <string name="recents_empty_message">No recent items</string>
@@ -237,7 +242,7 @@
     <!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
     <string name="toast_split_select_app">Tap another app to use split screen</string>
     <string name="toast_contextual_split_select_app">Choose another app to use split screen</string>
-    <string name="toast_split_select_app_cancel"><b>Cancel</b></string>
+    <string name="toast_split_select_app_cancel">Cancel</string>
     <string name="toast_split_select_cont_desc">Exit split screen selection</string>
     <!-- Label for toast when app selected for split isn't supported. [CHAR_LIMIT=50] -->
     <string name="toast_split_app_unsupported">Choose another app to use split screen</string>
@@ -299,10 +304,16 @@
     <string name="taskbar_button_quick_settings">Quick Settings</string>
     <!-- Accessibility title for the Taskbar window. [CHAR_LIMIT=NONE] -->
     <string name="taskbar_a11y_title">Taskbar</string>
-    <!-- Accessibility title for the Taskbar window appeared. [CHAR_LIMIT=NONE] -->
+    <!-- Accessibility title for the Taskbar window appeared. [CHAR_LIMIT=30] -->
     <string name="taskbar_a11y_shown_title">Taskbar shown</string>
-    <!-- Accessibility title for the Taskbar window being close. [CHAR_LIMIT=NONE] -->
+    <!-- Accessibility title for the Taskbar window appearing together with bubble bar on left. [CHAR_LIMIT=30] -->
+    <string name="taskbar_a11y_shown_with_bubbles_left_title">Taskbar &#38; bubbles left shown</string>
+    <!-- Accessibility title for the Taskbar window appearing together with bubble bar on right. [CHAR_LIMIT=30] -->
+    <string name="taskbar_a11y_shown_with_bubbles_right_title">Taskbar &#38; bubbles right shown</string>
+    <!-- Accessibility title for the Taskbar window being closed. [CHAR_LIMIT=30] -->
     <string name="taskbar_a11y_hidden_title">Taskbar hidden</string>
+    <!-- Accessibility title for the Taskbar window being closed together with bubble bar. [CHAR_LIMIT=30] -->
+    <string name="taskbar_a11y_hidden_with_bubbles_title">Taskbar &#38; bubbles hidden</string>
     <!-- Accessibility title for the Taskbar window on phones. [CHAR_LIMIT=NONE] -->
     <string name="taskbar_phone_a11y_title">Navigation bar</string>
     <!-- Text in popup dialog for user to switch between always showing Taskbar or not. [CHAR LIMIT=30] -->
@@ -311,6 +322,8 @@
     <string name="change_navigation_mode">Change navigation mode</string>
     <!-- Accessibility title for the Taskbar vertical divider icon. [CHAR_LIMIT=NONE] -->
     <string name="taskbar_divider_a11y_title">Taskbar Divider</string>
+    <!-- Accessibility title for the Taskbar Overflow icon. [CHAR_LIMIT=NONE] -->
+    <string name="taskbar_overflow_a11y_title">Taskbar Overflow</string>
 
 
     <!-- Label for moving drop target to the top or left side of the screen, depending on orientation (from the Taskbar only). -->
@@ -318,17 +331,14 @@
     <!-- Label for moving drop target to the bottom or right side of the screen, depending on orientation (from the Taskbar only). -->
     <string name="move_drop_target_bottom_or_right">Move to bottom&#47;right</string>
 
-    <!-- Label for quick switch tile showing how many more apps are available [CHAR LIMIT=NONE] -->
+    <!-- Label for quick switch tile showing how many more apps are available. The number will be displayed above this text. [CHAR LIMIT=NONE] -->
     <string name="quick_switch_overflow">{count, plural,
-            =1{Show # more app.}
-            other{Show # more apps.}
+            =1{more app}
+            other{more apps}
         }</string>
 
-    <!-- Label for quick switch tile showing how many apps are available in desktop mode [CHAR LIMIT=NONE] -->
-    <string name="quick_switch_desktop">{count, plural,
-            =1{Show # desktop app.}
-            other{Show # desktop apps.}
-        }</string>
+    <!-- Label for quick switch tile to launch desktop mode [CHAR LIMIT=NONE] -->
+    <string name="quick_switch_desktop">Desktop</string>
 
     <!-- Accessibility label for quick switch tiles showing split tasks [CHAR LIMIT=NONE] -->
     <string name="quick_switch_split_task"><xliff:g id="app_name_1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app_name_2" example="Gmail">%2$s</xliff:g></string>
@@ -342,4 +352,18 @@
     <string name="bubble_bar_bubble_description"><xliff:g id="notification_title" example="some title">%1$s</xliff:g> from <xliff:g id="app_name" example="YouTube">%2$s</xliff:g></string>
     <!-- Content description for bubble bar when it has multiple bubbles. [CHAR_LIMIT=NONE] -->
     <string name="bubble_bar_description_multiple_bubbles"><xliff:g id="bubble_bar_bubble_description" example="some title from YouTube">%1$s</xliff:g> and <xliff:g id="bubble_count" example="4">%2$d</xliff:g> more</string>
+    <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_move_left">Move left</string>
+    <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_move_right">Move right</string>
+    <!-- Action in accessibility menu to dismiss all bubbles. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_dismiss_all">Dismiss all</string>
+    <!-- Accessibility announcement when the bubble bar expands. [CHAR LIMIT=NONE]-->
+    <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 952505a..c423d09 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -124,7 +124,7 @@
     <style name="TextAppearance.GestureTutorial.ButtonLabel"
         parent="TextAppearance.GestureTutorial.CallToAction">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item>
+        <item name="android:textColor">?attr/materialColorOnPrimary</item>
         <item name="android:letterSpacing">0.02</item>
         <item name="android:textSize">16sp</item>
         <item name="android:textAllCaps">false</item>
@@ -205,7 +205,7 @@
         <item name="android:textColor">?attr/tutorialSubtitle</item>
     </style>
 
-    <style name="AllSetTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
+    <style name="AllSetTheme" parent="@style/DynamicColorsBaseLauncherTheme.NoActionBar">
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:statusBarColor">@android:color/transparent</item>
         <item name="android:enforceNavigationBarContrast">false</item>
@@ -268,53 +268,71 @@
     <style name="KeyboardQuickSwitchText">
         <item name="fontFamily">google-sans-text</item>
         <item name="android:textSize">14sp</item>
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
         <item name="lineHeight">20sp</item>
     </style>
 
+    <style name="KeyboardQuickSwitchText.LargeText" parent="KeyboardQuickSwitchText">
+        <item name="android:textSize">32sp</item>
+    </style>
+
     <style name="KeyboardQuickSwitchText.OnBackground" parent="KeyboardQuickSwitchText">
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
+    </style>
+
+    <style name="KeyboardQuickSwitchText.OnTaskView" parent="KeyboardQuickSwitchText">
+        <item name="android:requiresFadingEdge">horizontal</item>
+        <item name="android:fadeScrollbars">false</item>
+        <item name="android:fadingEdgeLength">20dp</item>
+        <item name="android:ellipsize">none</item>
+        <item name="android:singleLine">true</item>
     </style>
 
     <style name="GestureTutorialActivity" parent="@style/AppTheme">
         <item name="background">@android:color/transparent</item>
         <item name="tutorialSubtitle">@android:color/black</item>
-        <item name="surfaceContainer">?androidprv:attr/materialColorSurfaceContainer</item>
-        <item name="onSurfaceHome">?androidprv:attr/materialColorPrimaryFixed</item>
+        <item name="surfaceContainer">?attr/materialColorSurfaceContainer</item>
+        <item name="onSurfaceHome">?attr/materialColorPrimaryFixed</item>
         <item name="surfaceHome">@android:color/system_accent1_300</item>
-        <item name="secondaryHome">?androidprv:attr/materialColorOnPrimaryFixedVariant</item>
-        <item name="onSurfaceBack">?androidprv:attr/materialColorTertiaryFixed</item>
+        <item name="secondaryHome">?attr/materialColorOnPrimaryFixedVariant</item>
+        <item name="onSurfaceBack">?attr/materialColorTertiaryFixed</item>
         <item name="surfaceBack">@android:color/system_accent3_300</item>
-        <item name="secondaryBack">?androidprv:attr/materialColorOnTertiaryFixedVariant</item>
-        <item name="onSurfaceOverview">?androidprv:attr/materialColorPrimaryFixed</item>
+        <item name="secondaryBack">?attr/materialColorOnTertiaryFixedVariant</item>
+        <item name="onSurfaceOverview">?attr/materialColorPrimaryFixed</item>
         <item name="surfaceOverview">@android:color/system_accent2_300</item>
-        <item name="secondaryOverview">?androidprv:attr/materialColorOnSecondaryFixedVariant</item>
+        <item name="secondaryOverview">?attr/materialColorOnSecondaryFixedVariant</item>
     </style>
 
     <style name="rotate_prompt_title" parent="TextAppearance.GestureTutorial.Dialog.Title">
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
     </style>
 
     <style name="rotate_prompt_subtitle" parent="TextAppearance.GestureTutorial.Dialog.Subtitle">
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+        <item name="android:textColor">?attr/materialColorOnSurfaceVariant</item>
     </style>
 
     <style name="ArrowTipTaskbarStyle">
-        <item name="arrowTipBackground">?androidprv:attr/materialColorSurfaceContainer</item>
-        <item name="arrowTipTextColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="arrowTipBackground">?attr/materialColorSurfaceContainer</item>
+        <item name="arrowTipTextColor">?attr/materialColorOnSurface</item>
     </style>
 
     <style name="IconAppChipMenuTextStyle">
         <item name="android:fontFamily">google-sans-text-medium</item>
         <item name="android:textSize">@dimen/task_thumbnail_icon_menu_text_size</item>
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
         <item name="android:letterSpacing">0.025</item>
         <item name="android:lineHeight">20sp</item>
     </style>
 
-    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
-        <item name="widgetsTheme">@style/WidgetContainerTheme</item>
+    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation</item>
+
+        <item name="widgetsTheme">@style/WidgetContainerTheme</item>
         <item name="pageIndicatorDotColor">@color/page_indicator_dot_color_light</item>
     </style>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index d973149..a63ba0f 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -26,7 +26,7 @@
 import android.animation.AnimatorSet;
 import android.content.Context;
 import android.os.Handler;
-import android.os.RemoteException;
+import android.util.Log;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.RemoteAnimationTarget;
 
@@ -196,6 +196,7 @@
                 if (skipFirstFrame) {
                     // Because t=0 has the app icon in its original spot, we can skip the
                     // first frame and have the same movement one frame earlier.
+                    Log.d("b/311077782", "LauncherAnimationRunner.setAnimation");
                     mAnimator.setCurrentPlayTime(
                             Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration()));
                 }
@@ -208,7 +209,7 @@
          * animation finished runnable.
          */
         @Override
-        public void onAnimationFinished() throws RemoteException {
+        public void onAnimationFinished() {
             mASyncFinishRunnable.run();
         }
     }
@@ -238,12 +239,5 @@
         @Override
         @UiThread
         default void onAnimationCancelled() {}
-
-        /**
-         * Returns whether this animation factory supports a tightly coupled return animation.
-         */
-        default boolean supportsReturnTransition() {
-            return false;
-        }
     }
 }
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/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
index 962fd91..1161720 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
@@ -15,10 +15,19 @@
  */
 package com.android.launcher3;
 
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.allapps.AllAppsRecyclerView;
+import com.android.launcher3.allapps.SearchRecyclerView;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.uioverrides.PredictedAppIcon;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -26,14 +35,61 @@
 import java.util.List;
 
 public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate {
+    private QuickstepLauncher mLauncher;
 
     public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) {
         super(launcher);
+        mLauncher = launcher;
         mActions.put(PIN_PREDICTION, new LauncherAction(
                 PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P));
     }
 
     @Override
+    public void onPopulateAccessibilityEvent(View view, AccessibilityEvent event) {
+        super.onPopulateAccessibilityEvent(view, event);
+        // Scroll to the position if focused view in main allapps list and not completely visible.
+        scrollToPositionIfNeeded(view);
+    }
+
+    private void scrollToPositionIfNeeded(View view) {
+        if (!Flags.accessibilityScrollOnAllapps()) {
+            return;
+        }
+        AllAppsRecyclerView contentView = mLauncher.getAppsView().getActiveRecyclerView();
+        if (contentView instanceof SearchRecyclerView) {
+            return;
+        }
+        LinearLayoutManager layoutManager = (LinearLayoutManager) contentView.getLayoutManager();
+        if (layoutManager == null) {
+            return;
+        }
+        RecyclerView.ViewHolder vh = contentView.findContainingViewHolder(view);
+        if (vh == null) {
+            return;
+        }
+        int itemPosition = vh.getBindingAdapterPosition();
+        if (itemPosition == NO_POSITION) {
+            return;
+        }
+        int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
+        int lastCompletelyVisible = layoutManager.findLastCompletelyVisibleItemPosition();
+        boolean itemCompletelyVisible = firstCompletelyVisible <= itemPosition
+                && lastCompletelyVisible >= itemPosition;
+        if (itemCompletelyVisible) {
+            return;
+        }
+        RecyclerView.SmoothScroller smoothScroller =
+                new LinearSmoothScroller(mLauncher.asContext()) {
+                    @Override
+                    protected int getVerticalSnapPreference() {
+                        return LinearSmoothScroller.SNAP_TO_ANY;
+                    }
+                };
+        smoothScroller.setTargetPosition(itemPosition);
+        layoutManager.startSmoothScroll(smoothScroller);
+    }
+
+    @Override
     protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
         if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) {
             out.add(new LauncherAction(PIN_PREDICTION, R.string.pin_prediction,
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 5a74f4a..18337d3 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -46,17 +46,12 @@
 import static com.android.launcher3.Flags.enableContainerReturnAnimations;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.Utilities.mapBoundToRange;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_BACK_SWIPE_HOME_ANIMATION;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_SCRIM_FOR_APP_LAUNCH;
-import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
 import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
-import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.testing.shared.TestProtocol.WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE;
 import static com.android.launcher3.util.DisplayController.isTransientTaskbar;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -65,7 +60,6 @@
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
-import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
 import static com.android.quickstep.util.AnimUtils.clampToDuration;
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
@@ -90,7 +84,6 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.IBinder;
@@ -117,11 +110,13 @@
 import android.view.animation.PathInterpolator;
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
+import android.window.WindowAnimationState;
 
 import androidx.annotation.NonNull;
 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;
@@ -139,16 +134,17 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.DynamicResource;
-import com.android.launcher3.util.ObjectWrapper;
 import com.android.launcher3.util.RunnableList;
-import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.StableViewInfo;
 import com.android.launcher3.views.FloatingIconView;
-import com.android.launcher3.views.ScrimView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.quickstep.LauncherBackAnimationController;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.util.AlreadyStartedBackAnimState;
+import com.android.quickstep.util.AnimatorBackState;
+import com.android.quickstep.util.BackAnimState;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig;
@@ -175,6 +171,7 @@
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 
@@ -183,9 +180,6 @@
  */
 public class QuickstepTransitionManager implements OnDeviceProfileChangeListener {
 
-    private static final String TRANSITION_COOKIE_PREFIX =
-            "com.android.launcher3.QuickstepTransitionManager_activityLaunch";
-
     private static final boolean ENABLE_SHELL_STARTING_SURFACE =
             SystemProperties.getBoolean("persist.debug.shell_starting_surface", true);
 
@@ -259,7 +253,6 @@
     // Strong refs to runners which are cleared when the launcher activity is destroyed
     private RemoteAnimationFactory mWallpaperOpenRunner;
     private RemoteAnimationFactory mAppLaunchRunner;
-    private RemoteAnimationFactory mKeyguardGoingAwayRunner;
 
     private RemoteAnimationFactory mWallpaperOpenTransitionRunner;
     private RemoteTransition mLauncherOpenTransition;
@@ -328,7 +321,7 @@
      * @return ActivityOptions with remote animations that controls how the window of the opening
      * targets are displayed.
      */
-    public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
+    public ActivityOptionsWrapper getActivityLaunchOptions(View v, ItemInfo itemInfo) {
         boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
         RunnableList onEndCallback = new RunnableList();
 
@@ -355,14 +348,7 @@
         IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback);
         options.setOnAnimationAbortListener(endCallback);
         options.setOnAnimationFinishedListener(endCallback);
-
-        IBinder cookie = mAppLaunchRunner.supportsReturnTransition()
-                ? ((ContainerAnimationRunner) mAppLaunchRunner).getCookie() : null;
-        addLaunchCookie(cookie, (ItemInfo) v.getTag(), options);
-
-        // Register the return animation so it can be triggered on back from the app to home.
-        maybeRegisterAppReturnTransition(v);
-
+        options.setLaunchCookie(StableViewInfo.toLaunchCookie(itemInfo));
         return new ActivityOptionsWrapper(options, onEndCallback);
     }
 
@@ -375,21 +361,9 @@
         ItemInfo tag = (ItemInfo) v.getTag();
         ContainerAnimationRunner containerRunner = null;
         if (tag != null && tag.shouldUseBackgroundAnimation()) {
-            // The cookie should only override the default used by launcher if container return
-            // animations are enabled.
-            ActivityTransitionAnimator.TransitionCookie cookie =
-                    checkReturnAnimationsFlags()
-                            ? new ActivityTransitionAnimator.TransitionCookie(
-                                    TRANSITION_COOKIE_PREFIX + tag.id)
-                            : null;
-            ContainerAnimationRunner launchAnimationRunner =
-                    ContainerAnimationRunner.fromView(
-                            v, cookie, true /* forLaunch */, mLauncher, mStartingWindowListener,
-                            onEndCallback);
-
-            if (launchAnimationRunner != null) {
-                containerRunner = launchAnimationRunner;
-            }
+            containerRunner = ContainerAnimationRunner.fromView(
+                    v, true /* forLaunch */, mLauncher, mStartingWindowListener, onEndCallback,
+                    null /* windowState */);
         }
 
         mAppLaunchRunner = containerRunner != null
@@ -399,51 +373,6 @@
     }
 
     /**
-     * If container return animations are enabled and the current launch runner is itself a
-     * {@link ContainerAnimationRunner}, registers a matching return animation that de-registers
-     * itself after it has run once or is made obsolete by the view going away.
-     */
-    private void maybeRegisterAppReturnTransition(View v) {
-        if (!checkReturnAnimationsFlags() || !mAppLaunchRunner.supportsReturnTransition()) {
-            return;
-        }
-
-        ActivityTransitionAnimator.TransitionCookie cookie =
-                ((ContainerAnimationRunner) mAppLaunchRunner).getCookie();
-        RunnableList onEndCallback = new RunnableList();
-        ContainerAnimationRunner runner =
-                ContainerAnimationRunner.fromView(
-                        v, cookie, false /* forLaunch */, mLauncher, mStartingWindowListener,
-                        onEndCallback);
-        RemoteTransition transition =
-                new RemoteTransition(
-                        new LauncherAnimationRunner(
-                                mHandler, runner, true /* startAtFrontOfQueue */
-                        ).toRemoteTransition()
-                );
-
-        SystemUiProxy.INSTANCE.get(mLauncher).registerRemoteTransition(
-                transition, ContainerAnimationRunner.buildBackToHomeFilter(cookie, mLauncher));
-        ContainerAnimationRunner.setUpRemoteAnimationCleanup(
-                v, transition, onEndCallback, mLauncher);
-    }
-
-    /**
-     * Adds a new launch cookie for the activity launch if supported.
-     * Prioritizes the explicitly provided cookie, falling back on extracting one from the given
-     * {@link ItemInfo} if necessary.
-     */
-    private void addLaunchCookie(IBinder cookie, ItemInfo info, ActivityOptions options) {
-        if (cookie == null) {
-            cookie = mLauncher.getLaunchCookie(info);
-        }
-
-        if (cookie != null) {
-            options.setLaunchCookie(cookie);
-        }
-    }
-
-    /**
      * Whether the launch is a recents app transition and we should do a launch animation
      * from the recents view. Note that if the remote animation targets are not provided, this
      * may not always be correct as we may resolve the opening app to a task when the animation
@@ -646,56 +575,60 @@
         } 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);
             });
 
-            final boolean scrimEnabled = ENABLE_SCRIM_FOR_APP_LAUNCH.get();
-            if (scrimEnabled) {
-                int scrimColor = Themes.getAttrColor(mLauncher, R.attr.overviewScrimColor);
-                int scrimColorTrans = ColorUtils.setAlphaComponent(scrimColor, 0);
-                int[] colors = isAppOpening
-                        ? new int[]{scrimColorTrans, scrimColor}
-                        : new int[]{scrimColor, scrimColorTrans};
-                ScrimView scrimView = mLauncher.getScrimView();
-                if (scrimView.getBackground() instanceof ColorDrawable) {
-                    scrimView.setBackgroundColor(colors[0]);
-
-                    ObjectAnimator scrim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
-                            colors);
-                    scrim.setDuration(CONTENT_SCRIM_DURATION);
-                    scrim.setInterpolator(DECELERATE_1_5);
-
-                    launcherAnimator.play(scrim);
-                }
-            }
-
             endListener = () -> {
                 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 */);
+                    }
                 });
-                if (scrimEnabled) {
-                    mLauncher.getScrimView().setBackgroundColor(Color.TRANSPARENT);
-                }
                 mLauncher.resumeExpensiveViewUpdates();
             };
         }
@@ -1204,38 +1137,25 @@
      * additional animations.
      */
     private void addRemoteAnimations(RemoteAnimationDefinition definition) {
-        mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
+        mWallpaperOpenRunner = new WallpaperOpenLauncherAnimationRunner();
         definition.addRemoteAnimation(WindowManager.TRANSIT_OLD_WALLPAPER_OPEN,
                 WindowConfiguration.ACTIVITY_TYPE_STANDARD,
                 new RemoteAnimationAdapter(
                         new LauncherAnimationRunner(mHandler, mWallpaperOpenRunner,
                                 false /* startAtFrontOfQueue */),
                         CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
-
-        if (KEYGUARD_ANIMATION.get()) {
-            mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */);
-            definition.addRemoteAnimation(
-                    WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
-                    new RemoteAnimationAdapter(
-                            new LauncherAnimationRunner(
-                                    mHandler, mKeyguardGoingAwayRunner,
-                                    true /* startAtFrontOfQueue */),
-                            CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
-        }
     }
 
     /**
      * Registers remote animations used when closing apps to home screen.
      */
     public void registerRemoteTransitions() {
-        if (ENABLE_SHELL_TRANSITIONS) {
-            SystemUiProxy.INSTANCE.get(mLauncher).shareTransactionQueue();
-        }
+        SystemUiProxy.INSTANCE.get(mLauncher).shareTransactionQueue();
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
         }
 
-        mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */);
+        mWallpaperOpenTransitionRunner = new WallpaperOpenLauncherAnimationRunner();
         mLauncherOpenTransition = new RemoteTransition(
                 new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner,
                         false /* startAtFrontOfQueue */).toRemoteTransition(),
@@ -1244,15 +1164,25 @@
         TransitionFilter homeCheck = new TransitionFilter();
         // No need to handle the transition that also dismisses keyguard.
         homeCheck.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+
         homeCheck.mRequirements =
                 new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(),
+                        new TransitionFilter.Requirement(),
                         new TransitionFilter.Requirement()};
+
         homeCheck.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
         homeCheck.mRequirements[0].mTopActivity = mLauncher.getComponentName();
         homeCheck.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
         homeCheck.mRequirements[0].mOrder = CONTAINER_ORDER_TOP;
+
         homeCheck.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD;
         homeCheck.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+
+        homeCheck.mRequirements[2].mNot = true;
+        homeCheck.mRequirements[2].mCustomAnimation = true;
+        homeCheck.mRequirements[2].mMustBeTask = true;
+        homeCheck.mRequirements[2].mMustBeIndependent = true;
+
         SystemUiProxy.INSTANCE.get(mLauncher)
                 .registerRemoteTransition(mLauncherOpenTransition, homeCheck);
         if (mBackAnimationController != null) {
@@ -1280,13 +1210,10 @@
         // definition so we don't have to wait for the system gc
         mWallpaperOpenRunner = null;
         mAppLaunchRunner = null;
-        mKeyguardGoingAwayRunner = null;
     }
 
     protected void unregisterRemoteTransitions() {
-        if (ENABLE_SHELL_TRANSITIONS) {
-            SystemUiProxy.INSTANCE.get(mLauncher).unshareTransactionQueue();
-        }
+        SystemUiProxy.INSTANCE.get(mLauncher).unshareTransactionQueue();
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
         }
@@ -1340,41 +1267,6 @@
         return false;
     }
 
-    /**
-     * @return Runner that plays when user goes to Launcher
-     * ie. pressing home, swiping up from nav bar.
-     */
-    RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) {
-        return new WallpaperOpenLauncherAnimationRunner(fromUnlock);
-    }
-
-    /**
-     * Animator that controls the transformations of the windows when unlocking the device.
-     */
-    private Animator getUnlockWindowAnimator(RemoteAnimationTarget[] appTargets,
-            RemoteAnimationTarget[] wallpaperTargets) {
-        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
-        ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1);
-        unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
-        float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 :
-                QuickStepContract.getWindowCornerRadius(mLauncher);
-        unlockAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                SurfaceTransaction transaction = new SurfaceTransaction();
-                for (int i = appTargets.length - 1; i >= 0; i--) {
-                    RemoteAnimationTarget target = appTargets[i];
-                    transaction.forSurface(target.leash)
-                            .setAlpha(1f)
-                            .setWindowCrop(target.screenSpaceBounds)
-                            .setCornerRadius(cornerRadius);
-                }
-                surfaceApplier.scheduleApply(transaction);
-            }
-        });
-        return unlockAnimator;
-    }
-
     private static int getRotationChange(RemoteAnimationTarget[] appTargets) {
         int rotationChange = 0;
         for (RemoteAnimationTarget target : appTargets) {
@@ -1428,20 +1320,12 @@
 
         // Find the associated item info for the launch cookie (if available), note that predicted
         // apps actually have an id of -1, so use another default id here
-        final ArrayList<IBinder> launchCookies = runningTaskTarget.taskInfo.launchCookies == null
-                ? new ArrayList<>()
+        final List<IBinder> launchCookies = runningTaskTarget.taskInfo.launchCookies == null
+                ? Collections.EMPTY_LIST
                 : runningTaskTarget.taskInfo.launchCookies;
 
-        int launchCookieItemId = NO_MATCHING_ID;
-        for (IBinder cookie : launchCookies) {
-            Integer itemId = ObjectWrapper.unwrap(cookie);
-            if (itemId != null) {
-                launchCookieItemId = itemId;
-                break;
-            }
-        }
-
-        return mLauncher.getFirstMatchForAppClose(launchCookieItemId, packageName,
+        return mLauncher.getFirstMatchForAppClose(
+                StableViewInfo.fromLaunchCookies(launchCookies), packageName,
                 UserHandle.of(runningTaskTarget.taskInfo.userId), true /* supportsAllAppsState */);
     }
 
@@ -1497,8 +1381,13 @@
                             ? null
                             : mLauncher.getTaskbarUIController().findMatchingView(launcherView),
                     true /* hideOriginal */, targetRect, false /* isOpening */);
-            isInHotseat = launcherView.getTag() instanceof ItemInfo
-                    && ((ItemInfo) launcherView.getTag()).isInHotseat();
+            if (launcherView.getTag() instanceof ItemInfo itemInfo) {
+                isInHotseat = itemInfo.isInHotseat();
+                if (isInHotseat) {
+                    int dx = mLauncher.getHotseatItemTranslationX(itemInfo);
+                    targetRect.offset(dx, 0);
+                }
+            }
         } else {
             targetRect.set(getDefaultWindowTargetRect());
         }
@@ -1644,7 +1533,15 @@
     }
 
     private void addCujInstrumentation(Animator anim, int cuj) {
-        anim.addListener(new AnimationSuccessListener() {
+        anim.addListener(getCujAnimationSuccessListener(cuj));
+    }
+
+    private void addCujInstrumentation(RectFSpringAnim anim, int cuj) {
+        anim.addAnimatorListener(getCujAnimationSuccessListener(cuj));
+    }
+
+    private AnimationSuccessListener getCujAnimationSuccessListener(int cuj) {
+        return new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(Animator animation) {
                 mDragLayer.getViewTreeObserver().addOnDrawListener(
@@ -1678,27 +1575,55 @@
             public void onAnimationSuccess(Animator animator) {
                 InteractionJankMonitorWrapper.end(cuj);
             }
-        });
+        };
     }
 
     /**
      * Creates the {@link RectFSpringAnim} and {@link AnimatorSet} required to animate
      * the transition.
      */
-    public Pair<RectFSpringAnim, AnimatorSet> createWallpaperOpenAnimations(
+    @NonNull
+    public BackAnimState createWallpaperOpenAnimations(
             RemoteAnimationTarget[] appTargets,
-            RemoteAnimationTarget[] wallpaperTargets,
-            boolean fromUnlock,
+            RemoteAnimationTarget[] wallpapers,
+            RemoteAnimationTarget[] nonAppTargets,
             RectF startRect,
             float startWindowCornerRadius,
             boolean fromPredictiveBack) {
+        View launcherView = findLauncherView(appTargets);
+        if (checkReturnAnimationsFlags()
+                && launcherView != null
+                && launcherView.getTag() instanceof ItemInfo info
+                && info.shouldUseBackgroundAnimation()) {
+            // Try to create a return animation
+            RunnableList onEndCallback = new RunnableList();
+            WindowAnimationState windowState = new WindowAnimationState();
+            windowState.bounds = startRect;
+            windowState.bottomLeftRadius = windowState.bottomRightRadius =
+                    windowState.topLeftRadius = windowState.topRightRadius =
+                            startWindowCornerRadius;
+            ContainerAnimationRunner runner = ContainerAnimationRunner.fromView(
+                    launcherView, false /* forLaunch */, mLauncher, mStartingWindowListener,
+                    onEndCallback, windowState);
+            if (runner != null) {
+                runner.startAnimation(TRANSIT_CLOSE,
+                        appTargets, wallpapers, nonAppTargets,
+                        new IRemoteAnimationFinishedCallback.Stub() {
+                            @Override
+                            public void onAnimationFinished() {
+                                onEndCallback.executeAllAndDestroy();
+                            }
+                        });
+                return new AlreadyStartedBackAnimState(onEndCallback);
+            }
+        }
+
         AnimatorSet anim = new AnimatorSet();
         RectFSpringAnim rectFSpringAnim = null;
 
         final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
                 || launcherIsATargetWithMode(appTargets, MODE_OPENING);
 
-        View launcherView = findLauncherView(appTargets);
         boolean playFallBackAnimation = (launcherView == null
                 && launcherIsForceInvisibleOrOpening)
                 || mLauncher.getWorkspace().isOverlayShown()
@@ -1706,10 +1631,7 @@
 
         boolean playWorkspaceReveal = !fromPredictiveBack;
         boolean skipAllAppsScale = false;
-        if (fromUnlock) {
-            anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
-        } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
-                && !playFallBackAnimation) {
+        if (!playFallBackAnimation) {
             PointF velocity;
             if (enableScalingRevealHomeAnimation()) {
                 velocity = new PointF();
@@ -1759,10 +1681,6 @@
         // invisibility on touch down, and only reset it after the animation to home
         // is initialized.
         if (launcherIsForceInvisibleOrOpening || fromPredictiveBack) {
-            addCujInstrumentation(anim, playFallBackAnimation
-                    ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK
-                    : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
-
             AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -1772,6 +1690,14 @@
                 }
             };
 
+            if (rectFSpringAnim != null && anim.getChildAnimations().isEmpty()) {
+                addCujInstrumentation(rectFSpringAnim, Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
+            } else {
+                addCujInstrumentation(anim, playFallBackAnimation
+                        ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK
+                        : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
+            }
+
             if (fromPredictiveBack && rectFSpringAnim != null) {
                 rectFSpringAnim.addAnimatorListener(endListener);
             } else {
@@ -1797,7 +1723,7 @@
             }
         }
 
-        return new Pair(rectFSpringAnim, anim);
+        return new AnimatorBackState(rectFSpringAnim, anim);
     }
 
     public static int getTaskbarToHomeDuration() {
@@ -1817,12 +1743,6 @@
      */
     protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory {
 
-        private final boolean mFromUnlock;
-
-        public WallpaperOpenLauncherAnimationRunner(boolean fromUnlock) {
-            mFromUnlock = fromUnlock;
-        }
-
         @Override
         public void onAnimationStart(int transit,
                 RemoteAnimationTarget[] appTargets,
@@ -1853,14 +1773,14 @@
                 }
             }
 
-            Pair<RectFSpringAnim, AnimatorSet> pair = createWallpaperOpenAnimations(
-                    appTargets, wallpaperTargets, mFromUnlock, resolveRectF,
+            BackAnimState bankAnimState = createWallpaperOpenAnimations(
+                    appTargets, wallpaperTargets, nonAppTargets, resolveRectF,
                     QuickStepContract.getWindowCornerRadius(mLauncher),
                     false /* fromPredictiveBack */);
 
             TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets, false, null);
             mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
-            result.setAnimation(pair.second, mLauncher);
+            bankAnimState.applyToAnimationResult(result, mLauncher);
         }
     }
 
@@ -1928,29 +1848,19 @@
         /** The delegate runner that handles the actual animation. */
         private final RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> mDelegate;
 
-        @Nullable
-        private final ActivityTransitionAnimator.TransitionCookie mCookie;
-
         private ContainerAnimationRunner(
-                RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> delegate,
-                ActivityTransitionAnimator.TransitionCookie cookie) {
+                RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> delegate) {
             mDelegate = delegate;
-            mCookie = cookie;
-        }
-
-        @Nullable
-        ActivityTransitionAnimator.TransitionCookie getCookie() {
-            return mCookie;
         }
 
         @Nullable
         static ContainerAnimationRunner fromView(
                 View v,
-                ActivityTransitionAnimator.TransitionCookie cookie,
                 boolean forLaunch,
                 Launcher launcher,
                 StartingWindowListener startingWindowListener,
-                RunnableList onEndCallback) {
+                RunnableList onEndCallback,
+                @Nullable WindowAnimationState windowState) {
             if (!forLaunch && !checkReturnAnimationsFlags()) {
                 throw new IllegalStateException(
                         "forLaunch cannot be false when the enableContainerReturnAnimations or "
@@ -1960,7 +1870,7 @@
             // First the controller is created. This is used by the runner to animate the
             // origin/target view.
             ActivityTransitionAnimator.Controller controller =
-                    buildController(v, cookie, forLaunch);
+                    buildController(v, forLaunch, windowState);
             if (controller == null) {
                 return null;
             }
@@ -1985,8 +1895,7 @@
 
             return new ContainerAnimationRunner(
                     new ActivityTransitionAnimator.AnimationDelegate(
-                            MAIN_EXECUTOR, controller, callback, listener),
-                    cookie);
+                            MAIN_EXECUTOR, controller, callback, listener));
         }
 
         /**
@@ -1996,7 +1905,7 @@
          */
         @Nullable
         private static ActivityTransitionAnimator.Controller buildController(
-                View v, ActivityTransitionAnimator.TransitionCookie cookie, boolean isLaunching) {
+                View v, boolean isLaunching, @Nullable WindowAnimationState windowState) {
             View viewToUse = findLaunchableViewWithBackground(v);
             if (viewToUse == null) {
                 return null;
@@ -2027,8 +1936,8 @@
 
                 @Nullable
                 @Override
-                public ActivityTransitionAnimator.TransitionCookie getTransitionCookie() {
-                    return cookie;
+                public WindowAnimationState getWindowAnimatorState() {
+                    return windowState;
                 }
             };
         }
@@ -2042,81 +1951,26 @@
                 View view) {
             View current = view;
             while (current.getBackground() == null || !(current instanceof LaunchableView)) {
-                if (!(current.getParent() instanceof View)) {
+                if (current.getParent() instanceof View v) {
+                    current = v;
+                } else {
                     return null;
                 }
-
-                current = (View) current.getParent();
             }
-
             return (T) current;
         }
 
-        /**
-         * Builds the filter used by WM Shell to match app closing transitions (only back, no home
-         * button/gesture) to the given launch cookie.
-         */
-        static TransitionFilter buildBackToHomeFilter(
-                ActivityTransitionAnimator.TransitionCookie cookie, Launcher launcher) {
-            // Closing activity must include the cookie in its list of launch cookies.
-            TransitionFilter.Requirement appRequirement = new TransitionFilter.Requirement();
-            appRequirement.mActivityType = ACTIVITY_TYPE_STANDARD;
-            appRequirement.mLaunchCookie = cookie;
-            appRequirement.mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
-            // Opening activity must be Launcher.
-            TransitionFilter.Requirement launcherRequirement = new TransitionFilter.Requirement();
-            launcherRequirement.mActivityType = ACTIVITY_TYPE_HOME;
-            launcherRequirement.mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
-            launcherRequirement.mTopActivity = launcher.getComponentName();
-            // Transition types CLOSE and TO_BACK match the back button/gesture but not the  home
-            // button/gesture.
-            TransitionFilter filter = new TransitionFilter();
-            filter.mTypeSet = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
-            filter.mRequirements =
-                    new TransitionFilter.Requirement[]{appRequirement, launcherRequirement};
-            return filter;
-        }
-
-        /**
-         * Creates various conditions to ensure that the given transition is cleaned up correctly
-         * when necessary:
-         * - if the transition has run, it is the callback that unregisters it;
-         * - if the associated view is detached before the transition has had an opportunity to run,
-         *   a {@link View.OnAttachStateChangeListener} allows us to do the same (and removes
-         *   itself).
-         */
-        static void setUpRemoteAnimationCleanup(
-                View v, RemoteTransition transition, RunnableList callback, Launcher launcher) {
-            View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() {
-                @Override
-                public void onViewAttachedToWindow(@NonNull View v) {}
-
-                @Override
-                public void onViewDetachedFromWindow(@NonNull View v) {
-                    SystemUiProxy.INSTANCE.get(launcher)
-                            .unregisterRemoteTransition(transition);
-                    v.removeOnAttachStateChangeListener(this);
-                }
-            };
-
-            // Remove the animation as soon as it has run once.
-            callback.add(() -> {
-                SystemUiProxy.INSTANCE.get(launcher).unregisterRemoteTransition(transition);
-                if (v != null) {
-                    v.removeOnAttachStateChangeListener(listener);
-                }
-            });
-
-            // Remove the animation when the view is detached from the hierarchy.
-            // This is so that if back is not invoked (e.g. if we go back home through the home
-            // gesture) we don't have obsolete transitions staying registered.
-            v.addOnAttachStateChangeListener(listener);
-        }
-
         @Override
         public void onAnimationStart(int transit, RemoteAnimationTarget[] appTargets,
                 RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets,
                 LauncherAnimationRunner.AnimationResult result) {
+            startAnimation(
+                    transit, appTargets, wallpaperTargets, nonAppTargets, result);
+        }
+
+        public void startAnimation(int transit, RemoteAnimationTarget[] appTargets,
+                RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets,
+                IRemoteAnimationFinishedCallback result) {
             mDelegate.onAnimationStart(
                     transit, appTargets, wallpaperTargets, nonAppTargets, result);
         }
@@ -2125,11 +1979,6 @@
         public void onAnimationCancelled() {
             mDelegate.onAnimationCancelled();
         }
-
-        @Override
-        public boolean supportsReturnTransition() {
-            return true;
-        }
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 44d8a5c..dc0f899 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -20,10 +20,11 @@
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
 
-import static com.android.launcher3.Flags.enablePredictiveBackGesture;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import static java.util.Collections.emptyList;
+
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ClipData;
@@ -42,16 +43,19 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.dragndrop.SimpleDragLayer;
+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.popup.PopupDataProvider;
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
 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;
@@ -59,9 +63,8 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 
 /** An Activity that can host Launcher's widget picker. */
 public class WidgetPickerActivity extends BaseActivity {
@@ -109,8 +112,11 @@
     private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
     private WidgetsModel mModel;
     private LauncherAppState mApp;
+    private StringCache mStringCache;
     private WidgetPredictionsRequester mWidgetPredictionsRequester;
-    private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+    private final WidgetPickerDataProvider mWidgetPickerDataProvider =
+            new WidgetPickerDataProvider();
+    private WidgetsFilterDataProvider mWidgetsFilterDataProvider;
 
     private int mDesiredWidgetWidth;
     private int mDesiredWidgetHeight;
@@ -128,9 +134,23 @@
     /** A set of user ids that should be filtered out from the selected widgets. */
     @NonNull
     Set<Integer> mFilteredUserIds = new HashSet<>();
+
     @Nullable
     private WidgetsFullSheet mWidgetSheet;
 
+    private final Predicate<WidgetItem> mNoShortcutsFilter = widget -> {
+        final WidgetAcceptabilityVerdict verdict =
+                isWidgetAcceptable(widget, /* applySizeFilter=*/ false);
+        verdict.maybeLogVerdict();
+        return verdict.isAcceptable;
+    };
+    private final Predicate<WidgetItem> mHostSizeAndNoShortcutsFilter = widget -> {
+        final WidgetAcceptabilityVerdict verdict =
+                isWidgetAcceptable(widget, /* applySizeFilter=*/ true);
+        verdict.maybeLogVerdict();
+        return verdict.isAcceptable;
+    };
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -142,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);
@@ -156,11 +177,6 @@
 
     @Override
     protected void registerBackDispatcher() {
-        if (!enablePredictiveBackGesture()) {
-            super.registerBackDispatcher();
-            return;
-        }
-
         getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
                 OnBackInvokedDispatcher.PRIORITY_DEFAULT,
                 new BackAnimationCallback());
@@ -200,8 +216,8 @@
 
     @NonNull
     @Override
-    public PopupDataProvider getPopupDataProvider() {
-        return mPopupDataProvider;
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
     }
 
     @Override
@@ -271,47 +287,61 @@
         };
     }
 
-    /** Updates the model with widgets, applies filters and launches the widgets sheet once
-     * widgets are available */
+    /**
+     * Updates the model with widgets, applies filters and launches the widgets sheet once
+     * widgets are available
+     */
     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);
-            final List<WidgetsListBaseEntry> allWidgets =
-                    mModel.getFilteredWidgetsListForPicker(
-                            app.getContext(),
-                            /*widgetItemFilter=*/ widget -> {
-                                final WidgetAcceptabilityVerdict verdict =
-                                        isWidgetAcceptable(widget);
-                                verdict.maybeLogVerdict();
-                                return verdict.isAcceptable;
-                            }
-                    );
-            bindWidgets(allWidgets);
+
+            StringCache stringCache = new StringCache();
+            stringCache.loadStrings(this);
+
+            bindStringCache(stringCache);
+            bindWidgets(mModel.getWidgetsByPackageItem(), mModel.getDefaultWidgetsFilter());
             // Open sheet once widgets are available, so that it doesn't interrupt the open
             // animation.
             openWidgetsSheet();
             if (mUiSurface != null) {
-                Map<ComponentKey, WidgetItem> allWidgetItems = allWidgets.stream()
-                        .filter(entry -> entry instanceof WidgetsListContentEntry)
-                        .flatMap(entry -> entry.mWidgets.stream())
-                        .distinct()
-                        .collect(Collectors.toMap(
-                                widget -> new ComponentKey(widget.componentName, widget.user),
-                                Function.identity()
-                        ));
                 mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
-                        mUiSurface, allWidgetItems);
+                        mUiSurface, mModel.getWidgetsByComponentKey());
                 mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
             }
         });
     }
 
-    private void bindWidgets(List<WidgetsListBaseEntry> widgets) {
-        MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
+    private void bindStringCache(final StringCache stringCache) {
+        MAIN_EXECUTOR.execute(() -> mStringCache = stringCache);
     }
 
-   private void openWidgetsSheet() {
+    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, 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));
+    }
+
+    private void openWidgetsSheet() {
         MAIN_EXECUTOR.execute(() -> {
             mWidgetSheet = WidgetsFullSheet.show(this, true);
             mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
@@ -323,27 +353,34 @@
     private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
         // Bind recommendations once picker has finished open animation.
         MAIN_EXECUTOR.getHandler().postDelayed(
-                () -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets),
+                () -> mWidgetPickerDataProvider.setWidgetRecommendations(recommendedWidgets),
                 mDeviceProfile.bottomSheetOpenDuration);
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
+        MODEL_EXECUTOR.execute(() -> mWidgetsFilterDataProvider.destroy());
         if (mWidgetPredictionsRequester != null) {
             mWidgetPredictionsRequester.clear();
         }
     }
 
+    @Nullable
+    @Override
+    public StringCache getStringCache() {
+        return mStringCache;
+    }
+
     /**
      * 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();
             }
@@ -354,7 +391,7 @@
         }
 
         @Override
-        public void onBackInvoked() {
+        public void onBackInvokedCompat() {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
@@ -363,7 +400,7 @@
         }
 
         @Override
-        public void onBackProgressed(@NonNull BackEvent backEvent) {
+        public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
@@ -371,16 +408,22 @@
         }
 
         @Override
-        public void onBackCancelled() {
+        public void onBackCancelledCompat() {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
             mActiveOnBackAnimationCallback.onBackCancelled();
             mActiveOnBackAnimationCallback = null;
         }
-    };
+    }
 
-    private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
+    private boolean hasHostSizeFilters() {
+        // If optional filters such as size filter are present, we display them as default widgets.
+        return mDesiredWidgetWidth != 0 || mDesiredWidgetHeight != 0;
+    }
+
+    private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget,
+            boolean applySizeFilter) {
         final AppWidgetProviderInfo info = widget.widgetInfo;
         if (info == null) {
             return rejectWidget(widget, "shortcut");
@@ -401,61 +444,63 @@
                     info.widgetCategory);
         }
 
-        if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
-            // Accept the widget if the desired dimensions are unspecified.
-            return acceptWidget(widget);
-        }
-
-        final boolean isHorizontallyResizable =
-                (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
-        if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
-            if (info.maxResizeWidth > 0
-                    && info.maxResizeWidth >= info.minWidth
-                    && info.maxResizeWidth < mDesiredWidgetWidth) {
-                return rejectWidget(
-                        widget,
-                        "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
-                        info.maxResizeWidth,
-                        mDesiredWidgetWidth);
+        if (applySizeFilter) {
+            if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
+                // Accept the widget if the desired dimensions are unspecified.
+                return acceptWidget(widget);
             }
 
-            final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
-            if (minWidth > mDesiredWidgetWidth) {
-                return rejectWidget(
-                        widget,
-                        "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
-                        info.minWidth,
-                        info.minResizeWidth,
-                        mDesiredWidgetWidth);
-            }
-        }
+            final boolean isHorizontallyResizable =
+                    (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
+            if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
+                if (info.maxResizeWidth > 0
+                        && info.maxResizeWidth >= info.minWidth
+                        && info.maxResizeWidth < mDesiredWidgetWidth) {
+                    return rejectWidget(
+                            widget,
+                            "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
+                            info.maxResizeWidth,
+                            mDesiredWidgetWidth);
+                }
 
-        final boolean isVerticallyResizable =
-                (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
-        if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
-            if (info.maxResizeHeight > 0
-                    && info.maxResizeHeight >= info.minHeight
-                    && info.maxResizeHeight < mDesiredWidgetHeight) {
-                return rejectWidget(
-                        widget,
-                        "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
-                        info.maxResizeHeight,
-                        mDesiredWidgetHeight);
+                final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
+                if (minWidth > mDesiredWidgetWidth) {
+                    return rejectWidget(
+                            widget,
+                            "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
+                            info.minWidth,
+                            info.minResizeWidth,
+                            mDesiredWidgetWidth);
+                }
             }
 
-            final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
-            if (minHeight > mDesiredWidgetHeight) {
-                return rejectWidget(
-                        widget,
-                        "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
-                        info.minHeight,
-                        info.minResizeHeight,
-                        mDesiredWidgetHeight);
-            }
-        }
+            final boolean isVerticallyResizable =
+                    (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
+            if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
+                if (info.maxResizeHeight > 0
+                        && info.maxResizeHeight >= info.minHeight
+                        && info.maxResizeHeight < mDesiredWidgetHeight) {
+                    return rejectWidget(
+                            widget,
+                            "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
+                            info.maxResizeHeight,
+                            mDesiredWidgetHeight);
+                }
 
-        if (!isHorizontallyResizable || !isVerticallyResizable) {
-            return rejectWidget(widget, "not resizeable");
+                final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
+                if (minHeight > mDesiredWidgetHeight) {
+                    return rejectWidget(
+                            widget,
+                            "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
+                            info.minHeight,
+                            info.minResizeHeight,
+                            mDesiredWidgetHeight);
+                }
+            }
+
+            if (!isHorizontallyResizable || !isVerticallyResizable) {
+                return rejectWidget(widget, "not resizeable");
+            }
         }
 
         return acceptWidget(widget);
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 84c2ed2..32fda48 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -31,12 +31,13 @@
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.ColorInt;
-import androidx.core.content.ContextCompat;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.util.Themes;
 
 /**
  * A view which shows a horizontal divider
@@ -84,10 +85,9 @@
                 getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
         };
 
-        mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant);
+        mStrokeColor = Themes.getAttrColor(context, R.attr.materialColorOutlineVariant);
 
-        mAllAppsLabelTextColor = ContextCompat.getColor(context,
-                R.color.material_color_on_surface_variant);
+        mAllAppsLabelTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurfaceVariant);
 
         mAccessibilityManager = AccessibilityManager.getInstance(context);
         setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context));
@@ -254,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 a16031d..92d9516 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -290,6 +290,9 @@
         writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled);
         writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused);
         writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow);
-        writer.println(prefix + "\tmPredictedApps: " + mPredictedApps);
+        writer.println(prefix + "\tmPredictedApps: " + mPredictedApps.size());
+        for (WorkspaceItemInfo info : mPredictedApps) {
+            writer.println(prefix + "\t\t" + info);
+        }
     }
 }
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 9178062..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,7 +31,8 @@
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.views.DesktopTaskView
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
+import com.android.window.flags.Flags
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import java.util.function.Consumer
 
 /** Manage recents related operations with desktop tasks */
@@ -38,20 +40,22 @@
     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,
-        callback: Consumer<Boolean>? = null
+        animated: Boolean,
+        callback: Consumer<Boolean>? = null,
     ) {
         val animRunner =
             RemoteDesktopLaunchTransitionRunner(
                 desktopTaskView,
+                animated,
                 stateManager,
                 depthController,
-                callback
+                callback,
             )
         val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop")
         systemUiProxy.showDesktopApps(desktopTaskView.display.displayId, transition)
@@ -62,18 +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 {
@@ -83,16 +93,44 @@
                 }
             }
 
+            if (Flags.enableDesktopWindowingPersistence()) {
+                handleAnimationAfterReboot(info)
+            }
             MAIN_EXECUTOR.execute {
-                TaskViewUtils.composeRecentsDesktopLaunchAnimator(
-                    desktopTaskView,
-                    stateManager,
-                    depthController,
-                    info,
-                    t
+                val animator =
+                    TaskViewUtils.composeRecentsDesktopLaunchAnimator(
+                        desktopTaskView,
+                        stateManager,
+                        depthController,
+                        info,
+                        t,
+                    ) {
+                        errorHandlingFinishCallback.run()
+                        successCallback?.accept(true)
+                    }
+                if (!animated) {
+                    animator.setDuration(0)
+                }
+                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
                 ) {
-                    errorHandlingFinishCallback.run()
-                    successCallback?.accept(true)
+                    change.setStartAbsBounds(change.endAbsBounds)
                 }
             }
         }
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index de974ec..c50e82d 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -536,6 +536,9 @@
         writer.println(prefix + "HotseatPredictionController");
         writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags));
         writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount);
-        writer.println(prefix + "\tmPredictedItems: " + mPredictedItems);
+        writer.println(prefix + "\tmPredictedItems: " + mPredictedItems.size());
+        for (ItemInfo info : mPredictedItems) {
+            writer.println(prefix + "\t\t" + info);
+        }
     }
 }
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 64bb05e..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.getAllWidgetComponentsWithoutShortcuts();
+
+        // 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 62cc0bb..5744464 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -16,27 +16,32 @@
 package com.android.launcher3.statehandlers;
 
 import static android.view.View.VISIBLE;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
 
+import android.content.Context;
 import android.os.Debug;
-import android.os.SystemProperties;
 import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.fallback.RecentsState;
 import com.android.wm.shell.desktopmode.IDesktopTaskListener;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
+import java.io.PrintWriter;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -48,8 +53,8 @@
 
     private static final String TAG = "DesktopVisController";
     private static final boolean DEBUG = false;
-    private final Launcher mLauncher;
     private final Set<DesktopVisibilityListener> mDesktopVisibilityListeners = new HashSet<>();
+    private final Set<TaskbarDesktopModeListener> mTaskbarDesktopModeListeners = new HashSet<>();
 
     private int mVisibleDesktopTasksCount;
     private boolean mInOverviewState;
@@ -57,42 +62,46 @@
     private boolean mGestureInProgress;
 
     @Nullable
-    private IDesktopTaskListener mDesktopTaskListener;
+    private DesktopTaskListenerImpl mDesktopTaskListener;
 
-    public DesktopVisibilityController(Launcher launcher) {
-        mLauncher = launcher;
+    @Nullable
+    private Context mContext;
+
+    public DesktopVisibilityController(@NonNull Context context) {
+        setContext(context);
     }
 
-    /**
-     * Register a listener with System UI to receive updates about desktop tasks state
-     */
-    public void registerSystemUiListener() {
-        mDesktopTaskListener = new IDesktopTaskListener.Stub() {
-            @Override
-            public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
-                MAIN_EXECUTOR.execute(() -> {
-                    if (displayId == mLauncher.getDisplayId()) {
-                        if (DEBUG) {
-                            Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
-                        }
-                        setVisibleDesktopTasksCount(visibleTasksCount);
-                    }
-                });
-            }
+    /** Sets the context and re-registers the System Ui listener */
+    private void setContext(@Nullable Context context) {
+        unregisterSystemUiListener();
+        mContext = context;
+        registerSystemUiListener();
+    }
 
-            @Override
-            public void onStashedChanged(int displayId, boolean stashed) {
-              Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated");
-            }
-        };
-        SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(mDesktopTaskListener);
+    /** Register a listener with System UI to receive updates about desktop tasks state */
+    private void registerSystemUiListener() {
+        if (mContext == null) {
+            return;
+        }
+        if (mDesktopTaskListener != null) {
+            return;
+        }
+        mDesktopTaskListener = new DesktopTaskListenerImpl(this, mContext.getDisplayId());
+        SystemUiProxy.INSTANCE.get(mContext).setDesktopTaskListener(mDesktopTaskListener);
     }
 
     /**
      * Clear listener from System UI that was set with {@link #registerSystemUiListener()}
      */
-    public void unregisterSystemUiListener() {
-        SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null);
+    private void unregisterSystemUiListener() {
+        if (mContext == null) {
+            return;
+        }
+        if (mDesktopTaskListener == null) {
+            return;
+        }
+        SystemUiProxy.INSTANCE.get(mContext).setDesktopTaskListener(null);
+        mDesktopTaskListener.release();
         mDesktopTaskListener = null;
     }
 
@@ -125,11 +134,24 @@
         mDesktopVisibilityListeners.remove(listener);
     }
 
+    /** Registers a listener for Taskbar changes in Desktop Mode. */
+    public void registerTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) {
+        mTaskbarDesktopModeListeners.add(listener);
+    }
+
+    /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */
+    public void unregisterTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) {
+        mTaskbarDesktopModeListeners.remove(listener);
+    }
+
     /**
      * Sets the number of desktop windows that are visible and updates launcher visibility based on
      * it.
      */
     public void setVisibleDesktopTasksCount(int visibleTasksCount) {
+        if (mContext == null) {
+            return;
+        }
         if (DEBUG) {
             Log.d(TAG, "setVisibleDesktopTasksCount: visibleTasksCount=" + visibleTasksCount
                     + " currentValue=" + mVisibleDesktopTasksCount);
@@ -145,7 +167,8 @@
                 notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
             }
 
-            if (!enableDesktopWindowingWallpaperActivity() && wasVisible != isVisible) {
+            if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                    && wasVisible != isVisible) {
                 // TODO: b/333533253 - Remove after flag rollout
                 if (mVisibleDesktopTasksCount > 0) {
                     setLauncherViewsVisibility(View.INVISIBLE);
@@ -164,19 +187,33 @@
         }
     }
 
+    public void onLauncherStateChanged(LauncherState state) {
+        onLauncherStateChanged(
+                state, state == LauncherState.BACKGROUND_APP, state.isRecentsViewVisible);
+    }
+
+    public void onLauncherStateChanged(RecentsState state) {
+        onLauncherStateChanged(
+                state, state == RecentsState.BACKGROUND_APP, state.isRecentsViewVisible());
+    }
+
     /**
      * Process launcher state change and update launcher view visibility based on desktop state
      */
-    public void onLauncherStateChanged(LauncherState state) {
+    public void onLauncherStateChanged(
+            BaseState<?> state, boolean isBackgroundAppState, boolean isRecentsViewVisible) {
         if (DEBUG) {
             Log.d(TAG, "onLauncherStateChanged: newState=" + state);
         }
-        setBackgroundStateEnabled(state == BACKGROUND_APP);
+        setBackgroundStateEnabled(isBackgroundAppState);
         // Desktop visibility tracks overview and background state separately
-        setOverviewStateEnabled(state != BACKGROUND_APP && state.isRecentsViewVisible);
+        setOverviewStateEnabled(!isBackgroundAppState && isRecentsViewVisible);
     }
 
     private void setOverviewStateEnabled(boolean overviewStateEnabled) {
+        if (mContext == null) {
+            return;
+        }
         if (DEBUG) {
             Log.d(TAG, "setOverviewStateEnabled: enabled=" + overviewStateEnabled
                     + " currentValue=" + mInOverviewState);
@@ -189,7 +226,7 @@
                 notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
             }
 
-            if (enableDesktopWindowingWallpaperActivity()) {
+            if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
                 return;
             }
             // TODO: b/333533253 - Clean up after flag rollout
@@ -207,13 +244,26 @@
     }
 
     private void notifyDesktopVisibilityListeners(boolean areDesktopTasksVisible) {
+        if (mContext == null) {
+            return;
+        }
         if (DEBUG) {
             Log.d(TAG, "notifyDesktopVisibilityListeners: visible=" + areDesktopTasksVisible);
         }
         for (DesktopVisibilityListener listener : mDesktopVisibilityListeners) {
             listener.onDesktopVisibilityChanged(areDesktopTasksVisible);
         }
-        DisplayController.handleInfoChangeForDesktopMode(mLauncher);
+        DisplayController.INSTANCE.get(mContext).notifyConfigChange();
+    }
+
+    private void notifyTaskbarDesktopModeListeners(boolean doesAnyTaskRequireTaskbarRounding) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyTaskbarDesktopModeListeners: doesAnyTaskRequireTaskbarRounding="
+                    + doesAnyTaskRequireTaskbarRounding);
+        }
+        for (TaskbarDesktopModeListener listener : mTaskbarDesktopModeListeners) {
+            listener.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding);
+        }
     }
 
     /**
@@ -289,22 +339,32 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void setLauncherViewsVisibility(int visibility) {
-        if (enableDesktopWindowingWallpaperActivity()) {
+        if (mContext == null) {
+            return;
+        }
+        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             return;
         }
         if (DEBUG) {
             Log.d(TAG, "setLauncherViewsVisibility: visibility=" + visibility + " "
                     + Debug.getCaller());
         }
-        View workspaceView = mLauncher.getWorkspace();
-        if (workspaceView != null) {
-            workspaceView.setVisibility(visibility);
+        if (!(mContext instanceof ActivityContext activity)) {
+            return;
         }
-        View dragLayer = mLauncher.getDragLayer();
+        View dragLayer = activity.getDragLayer();
         if (dragLayer != null) {
             dragLayer.setVisibility(visibility);
         }
-        if (mLauncher instanceof QuickstepLauncher ql && ql.getTaskbarUIController() != null
+        if (!(activity instanceof Launcher launcher)) {
+            return;
+        }
+        View workspaceView = launcher.getWorkspace();
+        if (workspaceView != null) {
+            workspaceView.setVisibility(visibility);
+        }
+        if (launcher instanceof QuickstepLauncher ql
+                && ql.getTaskbarUIController() != null
                 && mVisibleDesktopTasksCount != 0) {
             ql.getTaskbarUIController().onLauncherVisibilityChanged(visibility == VISIBLE);
         }
@@ -314,14 +374,17 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void markLauncherPaused() {
-        if (enableDesktopWindowingWallpaperActivity()) {
+        if (mContext == null) {
+            return;
+        }
+        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             return;
         }
         if (DEBUG) {
             Log.d(TAG, "markLauncherPaused " + Debug.getCaller());
         }
         StatefulActivity<LauncherState> activity =
-                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext();
         if (activity != null) {
             activity.setPaused();
         }
@@ -331,14 +394,17 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void markLauncherResumed() {
-        if (enableDesktopWindowingWallpaperActivity()) {
+        if (mContext == null) {
+            return;
+        }
+        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             return;
         }
         if (DEBUG) {
             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.
@@ -347,6 +413,22 @@
         }
     }
 
+    public void onDestroy() {
+        setContext(null);
+    }
+
+    public void dumpLogs(String prefix, PrintWriter pw) {
+        pw.println(prefix + "DesktopVisibilityController:");
+
+        pw.println(prefix + "\tmDesktopVisibilityListeners=" + mDesktopVisibilityListeners);
+        pw.println(prefix + "\tmVisibleDesktopTasksCount=" + mVisibleDesktopTasksCount);
+        pw.println(prefix + "\tmInOverviewState=" + mInOverviewState);
+        pw.println(prefix + "\tmBackgroundStateEnabled=" + mBackgroundStateEnabled);
+        pw.println(prefix + "\tmGestureInProgress=" + mGestureInProgress);
+        pw.println(prefix + "\tmDesktopTaskListener=" + mDesktopTaskListener);
+        pw.println(prefix + "\tmContext=" + mContext);
+    }
+
     /** A listener for when the user enters/exits Desktop Mode. */
     public interface DesktopVisibilityListener {
         /**
@@ -356,4 +438,65 @@
          */
         void onDesktopVisibilityChanged(boolean visible);
     }
+
+    /**
+     * Wrapper for the IDesktopTaskListener stub to prevent lingering references to the launcher
+     * activity via the controller.
+     */
+    private static class DesktopTaskListenerImpl extends IDesktopTaskListener.Stub {
+
+        private DesktopVisibilityController mController;
+        private final int mDisplayId;
+
+        DesktopTaskListenerImpl(@NonNull DesktopVisibilityController controller, int displayId) {
+            mController = controller;
+            mDisplayId = displayId;
+        }
+
+        /**
+         * Clears any references to the controller.
+         */
+        void release() {
+            mController = null;
+        }
+
+        @Override
+        public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
+            MAIN_EXECUTOR.execute(() -> {
+                if (mController != null && displayId == mDisplayId) {
+                    if (DEBUG) {
+                        Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
+                    }
+                    mController.setVisibleDesktopTasksCount(visibleTasksCount);
+                }
+            });
+        }
+
+        @Override
+        public void onStashedChanged(int displayId, boolean stashed) {
+            Log.w(TAG, "DesktopTaskListenerImpl: onStashedChanged is deprecated");
+        }
+
+        @Override
+        public void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding) {
+            MAIN_EXECUTOR.execute(() -> {
+                if (mController != null && DesktopModeStatus.useRoundedCorners()) {
+                    Log.d(TAG, "DesktopTaskListenerImpl: doesAnyTaskRequireTaskbarRounding= "
+                            + doesAnyTaskRequireTaskbarRounding);
+                    mController.notifyTaskbarDesktopModeListeners(
+                            doesAnyTaskRequireTaskbarRounding);
+                }
+            });
+        }
+    }
+
+    /** A listener for Taskbar in Desktop Mode. */
+    public interface TaskbarDesktopModeListener {
+        /**
+         * Callback for when task is resized in desktop mode.
+         *
+         * @param doesAnyTaskRequireTaskbarRounding whether task requires taskbar corner roundness.
+         */
+        void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding);
+    }
 }
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/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
index c201236..a833ccf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
@@ -16,19 +16,24 @@
 package com.android.launcher3.taskbar;
 
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.SystemUiProxy;
 
 import java.util.ArrayList;
 import java.util.List;
 
 // TODO(b/218912746): Share more behavior to avoid all apps context depending directly on taskbar.
 /** Base for common behavior between taskbar window contexts. */
-public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext {
+public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext,
+        SystemShortcut.BubbleActivityStarter {
 
     protected final LayoutInflater mLayoutInflater;
     private final List<OnDeviceProfileChangeListener> mDPChangeListeners = new ArrayList<>();
@@ -48,6 +53,18 @@
         return mDPChangeListeners;
     }
 
+    @Override
+    public void showShortcutBubble(ShortcutInfo info) {
+        if (info == null) return;
+        SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
+    }
+
+    @Override
+    public void showAppBubble(Intent intent) {
+        if (intent == null || intent.getPackage() == null) return;
+        SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
+    }
+
     /** Callback invoked when a drag is initiated within this context. */
     public abstract void onDragStart();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index 06d9ee6..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();
-        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 46501c4..23a5a27 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -17,18 +17,21 @@
 
 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.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
-import com.android.quickstep.LauncherActivityInterface;
+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;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -37,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;
 
@@ -44,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;
@@ -59,12 +65,21 @@
     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;
 
     /** Initialize the controller. */
     public void init(@NonNull TaskbarControllers controllers) {
@@ -82,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));
         }
     }
 
@@ -93,31 +110,65 @@
         openQuickSwitchView(-1);
     }
 
+    /**
+     * Opens or closes the view in response to taskbar action. The view shows a filtered list of
+     * tasks.
+     * @param taskIdsToExclude A list of tasks to exclude in the opened view.
+     * @param onClosed A callback used to notify when the KQS view is closed.
+     */
+    void toggleQuickSwitchViewForTaskbar(@NonNull Set<Integer> taskIdsToExclude,
+            @NonNull Runnable onClosed) {
+        mOnClosed = onClosed;
+
+        // Close the view if its shown, and was opened from the taskbar.
+        if (mQuickSwitchViewController != null
+                && !mQuickSwitchViewController.isCloseAnimationRunning()
+                && mQuickSwitchViewController.wasOpenedFromTaskbar()) {
+            closeQuickSwitchView(true);
+            return;
+        }
+
+        openQuickSwitchView(-1, taskIdsToExclude, true);
+    }
+
     private void openQuickSwitchView(int currentFocusedIndex) {
+        openQuickSwitchView(currentFocusedIndex, Collections.emptySet(), false);
+    }
+
+    private void openQuickSwitchView(int currentFocusedIndex,
+            @NonNull Set<Integer> taskIdsToExclude,
+            boolean wasOpenedFromTaskbar) {
         if (mQuickSwitchViewController != null) {
-            if (!mQuickSwitchViewController.isCloseAnimationRunning()) {
+            if (!mQuickSwitchViewController.isCloseAnimationRunning()
+                    && mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
                 return;
             }
-            // Allow the KQS to be reopened during the close animation to make it more responsive
+
+            // Allow the KQS to be reopened during the close animation to make it more responsive.
+            // Similarly, if KQS was opened in different mode (from taskbar vs. keyboard event),
+            // close it so it can be reopened in the correct mode.
+            // TODO(b/368119679) Consider updating list of shown tasks in place, or at least reopen
+            // the view in the same vertical location.
             closeQuickSwitchView(false);
         }
-        TaskbarOverlayContext overlayContext =
-                mControllers.taskbarOverlayController.requestWindow();
+        mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
+        if (Flags.taskbarOverflow()) {
+            mOverlayContext.getDragLayer().addTouchController(this);
+        }
         KeyboardQuickSwitchView keyboardQuickSwitchView =
-                (KeyboardQuickSwitchView) overlayContext.getLayoutInflater()
+                (KeyboardQuickSwitchView) mOverlayContext.getLayoutInflater()
                         .inflate(
                                 R.layout.keyboard_quick_switch_view,
-                                overlayContext.getDragLayer(),
+                                mOverlayContext.getDragLayer(),
                                 /* attachToRoot= */ false);
         mQuickSwitchViewController = new KeyboardQuickSwitchViewController(
-                mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks);
+                mControllers, mOverlayContext, keyboardQuickSwitchView, mControllerCallbacks);
 
-        DesktopVisibilityController desktopController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
         final boolean onDesktop =
-                desktopController != null && desktopController.areDesktopTasksVisible();
+                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(
@@ -126,15 +177,21 @@
                     /* updateTasks= */ false,
                     currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
                             ? 0 : currentFocusedIndex,
-                    onDesktop);
+                    onDesktop,
+                    mHasDesktopTask,
+                    mWasDesktopTaskFilteredOut,
+                    wasOpenedFromTaskbar);
             return;
         }
 
+        mExcludedTaskIds = taskIdsToExclude;
         mTaskListChangeId = mModel.getTasks((tasks) -> {
+            mHasDesktopTask = false;
+            mWasDesktopTaskFilteredOut = false;
             if (onDesktop) {
-                processLoadedTasksOnDesktop(tasks);
+                processLoadedTasksOnDesktop(tasks, taskIdsToExclude);
             } else {
-                processLoadedTasks(tasks);
+                processLoadedTasks(tasks, taskIdsToExclude);
             }
             // Check if the first task is running after the recents model has updated so that we use
             // the correct index.
@@ -144,25 +201,49 @@
                     /* updateTasks= */ true,
                     currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
                             ? 0 : currentFocusedIndex,
-                    onDesktop);
+                    onDesktop,
+                    mHasDesktopTask,
+                    mWasDesktopTaskFilteredOut,
+                    wasOpenedFromTaskbar);
         });
     }
 
-    private void processLoadedTasks(List<GroupTask> tasks) {
+    private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
+        return Flags.taskbarOverflow() && taskIdsToExclude.contains(task.task1.key.id);
+    }
+
+    private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
         // Only store MAX_TASK tasks, from most to least recent
         Collections.reverse(tasks);
         mTasks = tasks.stream()
+                .filter(task -> !(task instanceof DesktopTask)
+                        && !shouldExcludeTask(task, taskIdsToExclude))
                 .limit(MAX_TASKS)
                 .collect(Collectors.toList());
-        mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS);
+
+        for (int i = 0; i < tasks.size(); i++) {
+            if (tasks.get(i) instanceof DesktopTask) {
+                mHasDesktopTask = true;
+                if (i < mTasks.size()) {
+                    mWasDesktopTaskFilteredOut = true;
+                }
+                break;
+            }
+        }
+
+        mNumHiddenTasks = Math.max(0,
+                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 {
@@ -189,6 +270,10 @@
             return;
         }
         mQuickSwitchViewController.closeQuickSwitchView(animate);
+        if (mOnClosed != null) {
+            mOnClosed.run();
+            mOnClosed = null;
+        }
     }
 
     /**
@@ -201,6 +286,27 @@
                 ? -1 : mQuickSwitchViewController.launchFocusedTask();
     }
 
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (mQuickSwitchViewController == null
+                || mOverlayContext == null
+                || !Flags.taskbarOverflow()) {
+            return false;
+        }
+
+        TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
+        if (ev.getAction() == MotionEvent.ACTION_DOWN
+                && !mQuickSwitchViewController.isEventOverKeyboardQuickSwitch(dragLayer, ev)) {
+            closeQuickSwitchView(true);
+        }
+        return false;
+    }
+
     void onDestroy() {
         if (mQuickSwitchViewController != null) {
             mQuickSwitchViewController.onDestroy();
@@ -214,6 +320,8 @@
         pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null));
         pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks);
         pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId);
+        pw.println(prefix + "\tmHasDesktopTask=" + mHasDesktopTask);
+        pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
         pw.println(prefix + "\tmTasks=[");
         for (GroupTask task : mTasks) {
             Task task1 = task.task1;
@@ -235,10 +343,6 @@
 
     class ControllerCallbacks {
 
-        int getTaskCount() {
-            return mTasks.size() + (mNumHiddenTasks == 0 ? 0 : 1);
-        }
-
         @Nullable
         GroupTask getTaskAt(int index) {
             return index < 0 || index >= mTasks.size() ? null : mTasks.get(index);
@@ -262,6 +366,11 @@
         }
 
         void onCloseComplete() {
+            if (Flags.taskbarOverflow() && mOverlayContext != null) {
+                mOverlayContext.getDragLayer()
+                        .removeTouchController(KeyboardQuickSwitchController.this);
+            }
+            mOverlayContext = null;
             mQuickSwitchViewController = null;
         }
 
@@ -279,5 +388,10 @@
         boolean isFirstTaskRunning() {
             return isTaskRunning(getTaskAt(0));
         }
+
+        boolean isAspectRatioSquare() {
+            return mControllers != null && LayoutUtils.isAspectRatioSquare(
+                    mControllers.taskbarActivityContext.getDeviceProfile().aspectRatio);
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 39b4f77..ce96556 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -36,22 +36,25 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
 import com.android.quickstep.util.BorderAnimator;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
-import java.util.function.Consumer;
-
 import kotlin.Unit;
 
+import java.util.function.Consumer;
+
 /**
  * A view that displays a recent task during a keyboard quick switch.
  */
 public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
 
     private static final float THUMBNAIL_BLUR_RADIUS = 1f;
+    private static final int INVALID_BORDER_RADIUS = -1;
 
     @ColorInt private final int mBorderColor;
+    @ColorInt private final int mBorderRadius;
 
     @Nullable private BorderAnimator mBorderAnimator;
 
@@ -87,6 +90,8 @@
 
         mBorderColor = ta.getColor(
                 R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR);
+        mBorderRadius = ta.getDimensionPixelSize(
+                R.styleable.TaskView_focusBorderRadius, INVALID_BORDER_RADIUS);
         ta.recycle();
     }
 
@@ -103,8 +108,10 @@
 
         Preconditions.assertNotNull(mContent);
         mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
-                /* borderRadiusPx= */ resources.getDimensionPixelSize(
-                        R.dimen.keyboard_quick_switch_task_view_radius),
+                /* borderRadiusPx= */ mBorderRadius != INVALID_BORDER_RADIUS
+                        ? mBorderRadius
+                        : resources.getDimensionPixelSize(
+                                R.dimen.keyboard_quick_switch_task_view_radius),
                 /* borderWidthPx= */ resources.getDimensionPixelSize(
                                 R.dimen.keyboard_quick_switch_border_width),
                 /* boundsBuilder= */ bounds -> {
@@ -167,6 +174,61 @@
         });
     }
 
+    protected void setThumbnailsForSplitTasks(
+            @NonNull Task task1,
+            @Nullable Task task2,
+            @Nullable ThumbnailUpdateFunction thumbnailUpdateFunction,
+            @Nullable IconUpdateFunction iconUpdateFunction,
+            @Nullable SplitBounds splitBounds) {
+        setThumbnails(task1, task2, thumbnailUpdateFunction, iconUpdateFunction);
+
+        if (splitBounds == null) {
+            return;
+        }
+
+
+        final boolean isLeftRightSplit = !splitBounds.appsStackedVertically;
+        final float leftOrTopTaskPercent = isLeftRightSplit
+                ? splitBounds.leftTaskPercent : splitBounds.topTaskPercent;
+
+        ConstraintLayout.LayoutParams leftTopParams = (ConstraintLayout.LayoutParams)
+                mThumbnailView1.getLayoutParams();
+        ConstraintLayout.LayoutParams rightBottomParams = (ConstraintLayout.LayoutParams)
+                mThumbnailView2.getLayoutParams();
+
+        if (isLeftRightSplit) {
+            // Set thumbnail view ratio in left right split mode.
+            leftTopParams.width = 0; // Set width to 0dp, so it uses the constraint dimension ratio.
+            leftTopParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            leftTopParams.matchConstraintPercentWidth = leftOrTopTaskPercent;
+            leftTopParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID;
+            leftTopParams.rightToLeft = R.id.thumbnail_2;
+            mThumbnailView1.setLayoutParams(leftTopParams);
+
+            rightBottomParams.width = 0;
+            rightBottomParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            rightBottomParams.matchConstraintPercentWidth = 1 - leftOrTopTaskPercent;
+            rightBottomParams.leftToRight = R.id.thumbnail_1;
+            rightBottomParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID;
+            mThumbnailView2.setLayoutParams(rightBottomParams);
+        } else {
+            // Set thumbnail view ratio in top bottom split mode.
+            leftTopParams.height = 0;
+            leftTopParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            leftTopParams.matchConstraintPercentHeight = leftOrTopTaskPercent;
+            leftTopParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
+            leftTopParams.bottomToTop = R.id.thumbnail_2;
+            mThumbnailView1.setLayoutParams(leftTopParams);
+
+            rightBottomParams.height = 0;
+            rightBottomParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            rightBottomParams.matchConstraintPercentHeight = 1 - leftOrTopTaskPercent;
+            rightBottomParams.topToBottom = R.id.thumbnail_1;
+            rightBottomParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+            mThumbnailView2.setLayoutParams(rightBottomParams);
+        }
+    }
+
     private void applyThumbnail(
             @Nullable ImageView thumbnailView,
             @Nullable Task task,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 0ba5de1..05d34b5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -17,8 +17,6 @@
 
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
 
-import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -36,14 +34,12 @@
 import android.view.ViewTreeObserver;
 import android.view.animation.Interpolator;
 import android.widget.HorizontalScrollView;
-import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.core.content.res.ResourcesCompat;
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.jank.Cuj;
@@ -52,7 +48,6 @@
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
@@ -61,8 +56,12 @@
 import java.util.Locale;
 
 /**
- * View that allows quick switching between recent tasks through keyboard alt-tab and alt-shift-tab
- * commands.
+ * View that allows quick switching between recent tasks.
+ *
+ * Can be access via:
+ * - keyboard alt-tab
+ * - alt-shift-tab
+ * - taskbar overflow button
  */
 public class KeyboardQuickSwitchView extends ConstraintLayout {
 
@@ -101,9 +100,13 @@
     private int mTaskViewWidth;
     private int mTaskViewHeight;
     private int mSpacing;
+    private int mSmallSpacing;
     private int mOutlineRadius;
     private boolean mIsRtl;
 
+    private int mOverviewTaskIndex = -1;
+    private int mDesktopTaskIndex = -1;
+
     @Nullable private AnimatorSet mOpenAnimation;
 
     @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks;
@@ -128,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);
@@ -140,6 +152,8 @@
         mTaskViewHeight = resources.getDimensionPixelSize(
                 R.dimen.keyboard_quick_switch_taskview_height);
         mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing);
+        mSmallSpacing = resources.getDimensionPixelSize(
+                R.dimen.keyboard_quick_switch_view_small_spacing);
         mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
         mIsRtl = Utilities.isRtl(resources);
     }
@@ -147,6 +161,7 @@
     private KeyboardQuickSwitchTaskView createAndAddTaskView(
             int index,
             boolean isFinalView,
+            boolean useSmallStartSpacing,
             @LayoutRes int resId,
             @NonNull LayoutInflater layoutInflater,
             @Nullable View previousView) {
@@ -155,7 +170,7 @@
         taskView.setId(View.generateViewId());
         taskView.setOnClickListener(v -> mViewCallbacks.launchTaskAt(index));
 
-        LayoutParams lp = new LayoutParams(mTaskViewWidth, mTaskViewHeight);
+        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
         // Create a left-to-right ordering of views (or right-to-left in RTL locales)
         if (previousView != null) {
             lp.startToEnd = previousView.getId();
@@ -165,7 +180,7 @@
         lp.topToTop = PARENT_ID;
         lp.bottomToBottom = PARENT_ID;
         // Add spacing between views
-        lp.setMarginStart(mSpacing);
+        lp.setMarginStart(useSmallStartSpacing ? mSmallSpacing : mSpacing);
         if (isFinalView) {
             // Add spacing to the end of the final view so that scrolling ends with some padding.
             lp.endToEnd = PARENT_ID;
@@ -184,62 +199,77 @@
             int numHiddenTasks,
             boolean updateTasks,
             int currentFocusIndexOverride,
-            @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) {
+            @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks,
+            boolean useDesktopTaskView) {
         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(
                     i,
-                    /* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0,
-                    groupTask instanceof DesktopTask
-                            ? R.layout.keyboard_quick_switch_textonly_taskview
+                    /* isFinalView= */ i == tasksToDisplay - 1
+                            && numHiddenTasks == 0 && !useDesktopTaskView,
+                    /* useSmallStartSpacing= */ false,
+                    mViewCallbacks.isAspectRatioSquare()
+                            ? R.layout.keyboard_quick_switch_taskview_square
                             : R.layout.keyboard_quick_switch_taskview,
                     layoutInflater,
                     previousTaskView);
 
-            if (groupTask instanceof DesktopTask desktopTask) {
-                HashMap<String, Integer> args = new HashMap<>();
-                args.put("count", desktopTask.tasks.size());
+            final boolean firstTaskIsLeftTopTask =
+                    groupTask.mSplitBounds == null
+                            || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id
+                            || groupTask.task2 == null;
 
-                currentTaskView.<ImageView>findViewById(R.id.icon).setImageDrawable(
-                        ResourcesCompat.getDrawable(resources, R.drawable.ic_desktop, theme));
-                currentTaskView.<TextView>findViewById(R.id.text).setText(new MessageFormat(
-                        resources.getString(R.string.quick_switch_desktop),
-                        Locale.getDefault()).format(args));
-            } else {
-                currentTaskView.setThumbnails(
-                        groupTask.task1,
-                        groupTask.task2,
-                        updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
-                        updateTasks ? mViewCallbacks::updateIconInBackground : null);
-            }
+            currentTaskView.setThumbnailsForSplitTasks(
+                    firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2,
+                    firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1,
+                    updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
+                    updateTasks ? mViewCallbacks::updateIconInBackground : null,
+                    groupTask.mSplitBounds);
+
             previousTaskView = currentTaskView;
         }
-
         if (numHiddenTasks > 0) {
             HashMap<String, Integer> args = new HashMap<>();
             args.put("count", numHiddenTasks);
 
+            mOverviewTaskIndex = getTaskCount();
             View overviewButton = createAndAddTaskView(
-                    MAX_TASKS,
-                    /* isFinalView= */ true,
-                    R.layout.keyboard_quick_switch_textonly_taskview,
+                    mOverviewTaskIndex,
+                    /* isFinalView= */ !useDesktopTaskView,
+                    /* useSmallStartSpacing= */ false,
+                    R.layout.keyboard_quick_switch_overview_taskview,
                     layoutInflater,
                     previousTaskView);
 
-            overviewButton.<ImageView>findViewById(R.id.icon).setImageDrawable(
-                    ResourcesCompat.getDrawable(resources, R.drawable.view_carousel, theme));
-            overviewButton.<TextView>findViewById(R.id.text).setText(new MessageFormat(
+            overviewButton.<TextView>findViewById(R.id.large_text).setText(
+                    String.format(Locale.getDefault(), "%d", numHiddenTasks));
+            overviewButton.<TextView>findViewById(R.id.small_text).setText(new MessageFormat(
                     resources.getString(R.string.quick_switch_overflow),
                     Locale.getDefault()).format(args));
+
+            previousTaskView = overviewButton;
         }
-        mDisplayingRecentTasks = !groupTasks.isEmpty();
+        if (useDesktopTaskView) {
+            mDesktopTaskIndex = getTaskCount();
+            View desktopButton = createAndAddTaskView(
+                    mDesktopTaskIndex,
+                    /* isFinalView= */ true,
+                    /* useSmallStartSpacing= */ numHiddenTasks > 0,
+                    R.layout.keyboard_quick_switch_desktop_taskview,
+                    layoutInflater,
+                    previousTaskView);
+
+            desktopButton.<TextView>findViewById(R.id.text).setText(
+                    resources.getString(R.string.quick_switch_desktop));
+        }
+        mDisplayingRecentTasks = !groupTasks.isEmpty() || useDesktopTaskView;
 
         getViewTreeObserver().addOnGlobalLayoutListener(
                 new ViewTreeObserver.OnGlobalLayoutListener() {
@@ -252,6 +282,18 @@
                 });
     }
 
+    int getOverviewTaskIndex() {
+        return mOverviewTaskIndex;
+    }
+
+    int getDesktopTaskIndex() {
+        return mDesktopTaskIndex;
+    }
+
+    void resetViewCallbacks() {
+        mViewCallbacks = null;
+    }
+
     protected Animator getCloseAnimation() {
         AnimatorSet closeAnimation = new AnimatorSet();
 
@@ -362,7 +404,7 @@
                     }
                 });
                 animateFocusMove(-1, Math.min(
-                        mContent.getChildCount() - 1,
+                        getTaskCount() - 1,
                         currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride));
                 displayedContent.setVisibility(VISIBLE);
                 setVisibility(VISIBLE);
@@ -569,7 +611,11 @@
 
     @Nullable
     protected KeyboardQuickSwitchTaskView getTaskAt(int index) {
-        return !mDisplayingRecentTasks || index < 0 || index >= mContent.getChildCount()
+        return !mDisplayingRecentTasks || index < 0 || index >= getTaskCount()
                 ? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index);
     }
+
+    public int getTaskCount() {
+        return mContent.getChildCount();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index d411ba6..390112e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -15,9 +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;
 
@@ -25,9 +31,17 @@
 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;
 import com.android.systemui.shared.recents.model.Task;
@@ -56,6 +70,10 @@
     private int mCurrentFocusIndex = -1;
 
     private boolean mOnDesktop;
+    private boolean mWasDesktopTaskFilteredOut;
+    private boolean mWasOpenedFromTaskbar;
+
+    private boolean mDetachingFromWindow = false;
 
     protected KeyboardQuickSwitchViewController(
             @NonNull TaskbarControllers controllers,
@@ -72,14 +90,33 @@
         return mCurrentFocusIndex;
     }
 
+    protected boolean wasOpenedFromTaskbar() {
+        return mWasOpenedFromTaskbar;
+    }
+
     protected void openQuickSwitchView(
             @NonNull List<GroupTask> tasks,
             int numHiddenTasks,
             boolean updateTasks,
             int currentFocusIndexOverride,
-            boolean onDesktop) {
+            boolean onDesktop,
+            boolean hasDesktopTask,
+            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,
@@ -87,7 +124,35 @@
                 numHiddenTasks,
                 updateTasks,
                 currentFocusIndexOverride,
-                mViewCallbacks);
+                mViewCallbacks,
+                /* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
+    }
+
+    protected void positionView(boolean wasOpenedFromTaskbar, boolean isTransientTaskbar) {
+        if (!wasOpenedFromTaskbar) {
+            // Keep the default positioning.
+            return;
+        }
+
+        BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(
+                mKeyboardQuickSwitchView.getLayoutParams());
+        final Resources resources = mKeyboardQuickSwitchView.getResources();
+        final int marginHorizontal = resources.getDimensionPixelSize(
+                R.dimen.keyboard_quick_switch_margin_ends);
+
+        final DeviceProfile dp = mControllers.taskbarActivityContext.getDeviceProfile();
+        // Calculate the additional margin space that the KQS should move up for the transient
+        // taskbar. The value of spaceForTaskbar is the distance between the bottom of the KQS
+        // view with 0 bottom margin to the top of the transient taskbar view.
+        final int spaceForTaskbar = isTransientTaskbar ? dp.taskbarHeight + dp.taskbarBottomMargin
+                - dp.stashedTaskbarHeight : 0;
+        final int marginBottom = spaceForTaskbar + resources.getDimensionPixelSize(
+                R.dimen.keyboard_quick_switch_margin_bottom);
+
+        lp.setMargins(marginHorizontal, 0, marginHorizontal, marginBottom);
+        lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
+        lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+        mKeyboardQuickSwitchView.setLayoutParams(lp);
     }
 
     boolean isCloseAnimationRunning() {
@@ -136,7 +201,7 @@
         }
         // If the user quick switches too quickly, updateCurrentFocusIndex might not have run.
         return launchTaskAt(mControllerCallbacks.isFirstTaskRunning()
-                && mControllerCallbacks.getTaskCount() > 1 ? 1 : 0);
+                && mKeyboardQuickSwitchView.getTaskCount() > 1 ? 1 : 0);
     }
 
     private int launchTaskAt(int index) {
@@ -144,6 +209,33 @@
             // Ignore taps on task views and alt key unpresses while the close animation is running.
             return -1;
         }
+        if (index == mKeyboardQuickSwitchView.getOverviewTaskIndex()) {
+            // If there is a desktop task view, then we should account for it when focusing the
+            // first hidden non-desktop task view in recents view
+            return mOnDesktop ? 1 : (mWasDesktopTaskFilteredOut ? index + 1 : index);
+        }
+        Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin(
+                mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
+        Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
+                Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
+        TaskbarActivityContext context = mControllers.taskbarActivityContext;
+        final RemoteTransition slideInTransition = new RemoteTransition(new SlideInRemoteTransition(
+                Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
+                context.getDeviceProfile().overviewPageSpacing,
+                QuickStepContract.getWindowCornerRadius(context),
+                AnimationUtils.loadInterpolator(
+                        context, android.R.interpolator.fast_out_extra_slow_in),
+                onStartCallback,
+                onFinishCallback),
+                "SlideInTransition");
+        if (index == mKeyboardQuickSwitchView.getDesktopTaskIndex()) {
+            UI_HELPER_EXECUTOR.execute(() ->
+                    SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
+                            .showDesktopApps(
+                                    mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
+                                    slideInTransition));
+            return -1;
+        }
         // Even with a valid index, this can be null if the user tries to quick switch before the
         // views have been added in the KeyboardQuickSwitchView.
         GroupTask task = mControllerCallbacks.getTaskAt(index);
@@ -154,21 +246,15 @@
             // Ignore attempts to run the selected task if it is already running.
             return -1;
         }
-
-        Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin(
-                mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
-        Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
-                Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
-        TaskbarActivityContext context = mControllers.taskbarActivityContext;
-        RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
-                Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
-                context.getDeviceProfile().overviewPageSpacing,
-                QuickStepContract.getWindowCornerRadius(context),
-                AnimationUtils.loadInterpolator(
-                        context, android.R.interpolator.fast_out_extra_slow_in),
-                onStartCallback,
-                onFinishCallback),
-                "SlideInTransition");
+        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,
@@ -180,7 +266,12 @@
 
     private void onCloseComplete() {
         mCloseAnimation = null;
-        mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+        // Reset the view callbacks to prevent `onDetachedFromWindow` getting called in response to
+        // the `removeView(mKeyboardQuickSwitchView)` call.
+        mKeyboardQuickSwitchView.resetViewCallbacks();
+        if (!mDetachingFromWindow) {
+            mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+        }
         mControllerCallbacks.onCloseComplete();
         InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
     }
@@ -195,6 +286,16 @@
         pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus());
         pw.println(prefix + "\tisCloseAnimationRunning=" + isCloseAnimationRunning());
         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 {
@@ -222,7 +323,7 @@
             boolean traverseBackwards = (keyCode == KeyEvent.KEYCODE_TAB && event.isShiftPressed())
                     || (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && isRTL)
                     || (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !isRTL);
-            int taskCount = mControllerCallbacks.getTaskCount();
+            int taskCount = mKeyboardQuickSwitchView.getTaskCount();
             int toIndex = mCurrentFocusIndex == -1
                     // Focus the second-most recent app if possible
                     ? (taskCount > 1 ? 1 : 0)
@@ -257,5 +358,15 @@
         void updateIconInBackground(Task task, Consumer<Task> callback) {
             mControllerCallbacks.updateIconInBackground(task, callback);
         }
+
+        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 779009a..4a94be7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -15,12 +15,14 @@
  */
 package com.android.launcher3.taskbar;
 
+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;
 import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
-import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -32,26 +34,25 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.quickstep.HomeVisibilityState;
-import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
 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;
 import java.util.Arrays;
@@ -128,8 +129,8 @@
 
     @Override
     protected void onDestroy() {
+        onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */);
         super.onDestroy();
-        onLauncherVisibilityChanged(false);
         mTaskbarLauncherStateController.onDestroy();
 
         mLauncher.setTaskbarUIController(null);
@@ -160,6 +161,16 @@
                 shouldDelayLauncherStateAnim);
     }
 
+    @Override
+    public void stashHotseat(boolean stash) {
+        mTaskbarLauncherStateController.stashHotseat(stash);
+    }
+
+    @Override
+    public void unStashHotseatInstantly() {
+        mTaskbarLauncherStateController.unStashHotseatInstantly();
+    }
+
     /**
      * Adds the Launcher resume animator to the given animator set.
      *
@@ -184,44 +195,51 @@
      */
     @Override
     public void onLauncherVisibilityChanged(boolean isVisible) {
+        if (DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(mLauncher)) {
+            DisplayController.INSTANCE.get(mLauncher).notifyConfigChange();
+        }
         onLauncherVisibilityChanged(isVisible, false /* fromInit */);
     }
 
-    private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInit) {
+    private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInitOrDestroy) {
         onLauncherVisibilityChanged(
                 isVisible,
-                fromInit,
+                fromInitOrDestroy,
                 /* startAnimation= */ true,
-                DisplayController.isTransientTaskbar(mLauncher)
-                        ? TRANSIENT_TASKBAR_TRANSITION_DURATION
-                        : (!isVisible
-                                ? QuickstepTransitionManager.TASKBAR_TO_APP_DURATION
-                                : QuickstepTransitionManager.getTaskbarToHomeDuration()));
+                getTaskbarAnimationDuration(isVisible));
+    }
+
+    private int getTaskbarAnimationDuration(boolean isVisible) {
+        if (isVisible && !mLauncher.getPredictiveBackToHomeInProgress()) {
+            return getTaskbarToHomeDuration();
+        } else {
+            return DisplayController.isTransientTaskbar(mLauncher)
+                    ? TRANSIENT_TASKBAR_TRANSITION_DURATION
+                    : TASKBAR_TO_APP_DURATION;
+        }
     }
 
     @Nullable
     private Animator onLauncherVisibilityChanged(
-            boolean isVisible, boolean fromInit, boolean startAnimation, int duration) {
+            boolean isVisible, boolean fromInitOrDestroy, boolean startAnimation, int duration) {
         // Launcher is resumed during the swipe-to-overview gesture under shell-transitions, so
         // avoid updating taskbar state in that situation (when it's non-interactive -- or
         // "background") to avoid premature animations.
-        if (ENABLE_SHELL_TRANSITIONS && isVisible
-                && mLauncher.getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)
-                && !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) {
+        LauncherState state = mLauncher.getStateManager().getState();
+        boolean nonInteractiveState = state.hasFlag(FLAG_NON_INTERACTIVE)
+                && !state.isTaskbarAlignedWithHotseat(mLauncher);
+        if (isVisible && (nonInteractiveState || mSkipLauncherVisibilityChange)) {
             return null;
         }
 
-        DesktopVisibilityController desktopController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-        if (!enableDesktopWindowingWallpaperActivity()
-                && desktopController != null
-                && desktopController.areDesktopTasksVisible()) {
+        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) {
             // TODO: b/333533253 - Remove after flag rollout
             isVisible = false;
         }
 
         mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, isVisible);
-        if (fromInit) {
+        if (fromInitOrDestroy) {
             duration = 0;
         }
         return mTaskbarLauncherStateController.applyState(duration, startAnimation);
@@ -262,7 +280,12 @@
     }
 
     public boolean isDraggingItem() {
-        return mControllers.taskbarDragController.isDragging();
+        boolean bubblesDragging = false;
+        if (mControllers.bubbleControllers.isPresent()) {
+            bubblesDragging =
+                    mControllers.bubbleControllers.get().bubbleDragController.isDragging();
+        }
+        return mControllers.taskbarDragController.isDragging() || bubblesDragging;
     }
 
     @Override
@@ -339,16 +362,22 @@
             // This method can be called before init() is called.
             return;
         }
-        if (mControllers.uiController.isIconAlignedWithHotseat()
-                && !mTaskbarLauncherStateController.isAnimatingToLauncher()) {
-            // Only animate the nav buttons while home and not animating home, otherwise let
-            // the TaskbarViewController handle it.
-            mControllers.navbarButtonsViewController
-                    .getTaskbarNavButtonTranslationYForInAppDisplay()
-                    .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
-                            * mTaskbarInAppDisplayProgress.value);
-            mControllers.navbarButtonsViewController
-                    .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
+        if (mControllers.uiController.isIconAlignedWithHotseat()) {
+            if (!mTaskbarLauncherStateController.isAnimatingToLauncher()) {
+                // Only animate the nav buttons while home and not animating home, otherwise let
+                // the TaskbarViewController handle it.
+                mControllers.navbarButtonsViewController
+                        .getTaskbarNavButtonTranslationYForInAppDisplay()
+                        .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
+                                * mTaskbarInAppDisplayProgress.value);
+                mControllers.navbarButtonsViewController
+                        .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
+            }
+            if (isBubbleBarEnabled()) {
+                mControllers.bubbleControllers.ifPresent(
+                        c -> c.bubbleStashController.setInAppDisplayOverrideProgress(
+                                mTaskbarInAppDisplayProgress.value));
+            }
         }
     }
 
@@ -452,4 +481,25 @@
     protected String getTaskbarUIControllerName() {
         return "LauncherTaskbarUIController";
     }
+
+    @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.
+        if (mLauncher.getWorkspace().isOverlayShown()) {
+            mLauncher.getWorkspace().onOverlayScrollChanged(0);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 0fa3fbc..fbd1b6e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.LauncherAnimUtils.ROTATION_DRAWABLE_PERCENT;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
@@ -48,8 +49,10 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
-import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
+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;
@@ -64,22 +67,22 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.Region.Op;
-import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PaintDrawable;
 import android.graphics.drawable.RotateDrawable;
 import android.inputmethodservice.InputMethodService;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.util.Property;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
-import android.view.View.OnClickListener;
-import android.view.View.OnHoverListener;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
+import android.view.inputmethod.Flags;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -94,6 +97,7 @@
 import com.android.launcher3.anim.AlphaUpdateListener;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
+import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory;
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter;
 import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
@@ -106,10 +110,10 @@
 import com.android.systemui.shared.navigationbar.KeyButtonRipple;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
 import com.android.systemui.shared.rotation.RotationButton;
-import com.android.systemui.shared.rotation.RotationButtonController;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -119,7 +123,8 @@
 /**
  * Controller for managing nav bar buttons in taskbar
  */
-public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController {
+public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController,
+        BubbleBarController.BubbleBarLocationListener {
 
     private final Rect mTempRect = new Rect();
 
@@ -171,11 +176,14 @@
     // Used for IME+A11Y buttons
     private final ViewGroup mEndContextualContainer;
     private final ViewGroup mStartContextualContainer;
-    private final int mLightIconColorOnHome;
-    private final int mDarkIconColorOnHome;
-    /** Color to use for navigation bar buttons, if they are on on a Taskbar surface background. */
+    private final int mLightIconColorOnWorkspace;
+    private final int mDarkIconColorOnWorkspace;
+    /** Color to use for navbar buttons, if they are on on a Taskbar surface background. */
     private final int mOnBackgroundIconColor;
 
+    private @Nullable Animator mNavBarLocationAnimator;
+    private @Nullable BubbleBarLocation mBubbleBarTargetLocation;
+
     private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat(
             this::updateNavButtonTranslationY);
     private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat(
@@ -186,7 +194,10 @@
     // Used for System UI state updates that should translate the nav button for in-app display.
     private final AnimatedFloat mNavButtonInAppDisplayProgressForSysui = new AnimatedFloat(
             this::updateNavButtonInAppDisplayProgressForSysui);
-    /** Expected nav button dark intensity communicated via the framework. */
+    /**
+     * Expected nav button dark intensity piped down from {@code LightBarController} in framework
+     * via {@code TaskbarDelegate}.
+     */
     private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat(
             this::onDarkIntensityChanged);
     /** {@code 1} if the Taskbar background color is fully opaque. */
@@ -241,13 +252,13 @@
         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);
 
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
         }
     }
@@ -264,17 +275,28 @@
         boolean isThreeButtonNav = mContext.isThreeButtonNav();
         DeviceProfile deviceProfile = mContext.getDeviceProfile();
         Resources resources = mContext.getResources();
-        Point p = !mContext.isUserSetupComplete()
-                ? new Point(0, mControllers.taskbarActivityContext.getSetupWindowSize())
-                : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
-                        mContext.isPhoneMode());
-        mNavButtonsView.getLayoutParams().height = p.y;
+
+        int setupSize = mControllers.taskbarActivityContext.getSetupWindowSize();
+        Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
+                mContext.isPhoneMode(), mContext.isGestureNav());
+        ViewGroup.LayoutParams navButtonsViewLayoutParams = mNavButtonsView.getLayoutParams();
+        navButtonsViewLayoutParams.width = p.x;
+        if (!mContext.isUserSetupComplete()) {
+            // Setup mode in phone mode uses gesture nav.
+            navButtonsViewLayoutParams.height = setupSize;
+        } else {
+            navButtonsViewLayoutParams.height = p.y;
+        }
+        mNavButtonsView.setLayoutParams(navButtonsViewLayoutParams);
 
         mIsImeRenderingNavButtons =
                 InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar();
         if (!mIsImeRenderingNavButtons) {
             // IME switcher
-            mImeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH,
+            final int switcherResId = Flags.imeSwitcherRevamp()
+                    ? com.android.internal.R.drawable.ic_ime_switcher_new
+                    : R.drawable.ic_ime_switcher;
+            mImeSwitcherButton = addButton(switcherResId, BUTTON_IME_SWITCH,
                     isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer,
                     mControllers.navButtonController, R.id.ime_switcher);
             mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton,
@@ -293,8 +315,13 @@
                         .get(ALPHA_INDEX_SMALL_SCREEN),
                 flags -> (flags & FLAG_SMALL_SCREEN) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
-                .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        if (!mContext.isPhoneMode()) {
+            mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
+                    .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        }
+
+        // Start at 1 because relevant flags are unset at init.
+        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
 
         // Force nav buttons (specifically back button) to be visible during setup wizard.
         boolean isInSetup = !mContext.isUserSetupComplete();
@@ -306,39 +333,41 @@
         // - IME is showing (add separate translation for IME)
         // - VoiceInteractionWindow (assistant) is showing
         // - Keyboard shortcuts helper is showing
-        int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
-                | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
-        mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
-                flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
-                1, 0));
-        // Center nav buttons in new height for IME.
-        float transForIme = (mContext.getDeviceProfile().taskbarHeight
-                - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
-        // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
-        float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
-        mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
-                flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
-                transForIme, defaultButtonTransY));
+        if (!mContext.isPhoneMode()) {
+            int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
+                    | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
+            mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
+                    flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
+                    1, 0));
+            // Center nav buttons in new height for IME.
+            float transForIme = (mContext.getDeviceProfile().taskbarHeight
+                    - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
+            // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
+            float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
+            mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
+                    flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
+                    transForIme, defaultButtonTransY));
 
-        // Start at 1 because relevant flags are unset at init.
-        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
-        mPropertyHolders.add(new StatePropertyHolder(
-                mOnBackgroundNavButtonColorOverrideMultiplier,
-                flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mOnBackgroundNavButtonColorOverrideMultiplier,
+                    flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(
-                mSlideInViewVisibleNavButtonColorOverride,
-                flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mSlideInViewVisibleNavButtonColorOverride,
+                    flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+        }
 
         if (alwaysShowButtons) {
             initButtons(mNavButtonContainer, mEndContextualContainer,
                     mControllers.navButtonController);
             updateButtonLayoutSpacing();
-            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneButtonNavMode());
+            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneMode());
 
-            mPropertyHolders.add(new StatePropertyHolder(
-                    mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
-                    flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            if (!mContext.isPhoneMode()) {
+                mPropertyHolders.add(new StatePropertyHolder(
+                        mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
+                        flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            }
         } else if (!mIsImeRenderingNavButtons) {
             View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
                     mStartContextualContainer, mControllers.navButtonController, R.id.back);
@@ -361,7 +390,7 @@
                 R.bool.floating_rotation_button_position_left);
         mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
                 mRotationButtonListener);
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.init();
         }
 
@@ -369,7 +398,7 @@
         mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
 
         // Initialize things needed to move nav buttons to separate window.
-        mSeparateWindowParent = new BaseDragLayer<TaskbarActivityContext>(mContext, null, 0) {
+        mSeparateWindowParent = new BaseDragLayer<>(mContext, null, 0) {
             @Override
             public void recreateControllers() {
                 mControllers = new TouchController[0];
@@ -383,6 +412,12 @@
             }
         };
         mSeparateWindowParent.recreateControllers();
+        if (BubbleBarController.isBubbleBarEnabled()) {
+            mNavButtonsView.addOnLayoutChangeListener(
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+                            onLayoutsUpdated()
+            );
+        }
     }
 
     private void initButtons(ViewGroup navContainer, ViewGroup endContainer,
@@ -625,7 +660,7 @@
     }
 
     public void setWallpaperVisible(boolean isVisible) {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.setWallpaperVisibility(isVisible);
         }
     }
@@ -638,20 +673,20 @@
     }
 
     public void checkNavBarModes() {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
             mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden);
         }
     }
 
     public void finishBarAnimations() {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.finishAnimations();
         }
     }
 
     public void touchAutoDim(boolean reset) {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.setAutoDim(false);
             mHandler.removeCallbacks(mAutoDim);
             if (reset) {
@@ -661,7 +696,7 @@
     }
 
     public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.transitionTo(barMode, animate);
         }
     }
@@ -700,7 +735,7 @@
     private void applyState() {
         int count = mPropertyHolders.size();
         for (int i = 0; i < count; i++) {
-            mPropertyHolders.get(i).setState(mState);
+            mPropertyHolders.get(i).setState(mState, mContext.isGestureNav());
         }
     }
 
@@ -733,40 +768,68 @@
         mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY);
     }
 
+    /**
+     * Sets Taskbar 3-button mode icon colors based on the
+     * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. For certain cases
+     * in large screen taskbar where there may be opaque surfaces, the selected SystemUI button
+     * colors are intentionally overridden.
+     * <p>
+     * This method is also called when any of the AnimatedFloat instances change.
+     */
     private void updateNavButtonColor() {
         final ArgbEvaluator argbEvaluator = ArgbEvaluator.getInstance();
-        final int sysUiNavButtonIconColorOnHome = (int) argbEvaluator.evaluate(
-                mTaskbarNavButtonDarkIntensity.value,
-                mLightIconColorOnHome,
-                mDarkIconColorOnHome);
-
-        final int iconColor;
-        if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode()) {
-            iconColor = sysUiNavButtonIconColorOnHome;
-        } else {
-            // Override the color from framework if nav buttons are over an opaque Taskbar surface.
-            iconColor = (int) argbEvaluator.evaluate(
-                    mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max(
-                            mOnTaskbarBackgroundNavButtonColorOverride.value,
-                            mSlideInViewVisibleNavButtonColorOverride.value),
-                    sysUiNavButtonIconColorOnHome,
-                    mOnBackgroundIconColor);
+        int taskbarNavButtonColor = getSysUiIconColorOnHome(argbEvaluator);
+        // Only phone mode foldable button colors should be identical to SysUI navbar colors.
+        if (!(ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode())) {
+            taskbarNavButtonColor = getTaskbarButtonColor(argbEvaluator, taskbarNavButtonColor);
         }
+        applyButtonColors(taskbarNavButtonColor);
+    }
 
+    /**
+     * Taskbar 3-button mode icon colors based on the
+     * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework.
+     */
+    private int getSysUiIconColorOnHome(ArgbEvaluator argbEvaluator) {
+        return (int) argbEvaluator.evaluate(getTaskbarNavButtonDarkIntensity().value,
+                mLightIconColorOnWorkspace, mDarkIconColorOnWorkspace);
+    }
+
+    /**
+     * If Taskbar background is opaque or slide in overlay is visible, the selected SystemUI button
+     * colors are intentionally overridden. The override can be disabled when
+     * {@link #mOnBackgroundNavButtonColorOverrideMultiplier} is {@code 0}.
+     */
+    private int getTaskbarButtonColor(ArgbEvaluator argbEvaluator, int sysUiIconColorOnHome) {
+        final float sysUIColorOverride =
+                mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max(
+                        mOnTaskbarBackgroundNavButtonColorOverride.value,
+                        mSlideInViewVisibleNavButtonColorOverride.value);
+        return (int) argbEvaluator.evaluate(sysUIColorOverride, sysUiIconColorOnHome,
+                mOnBackgroundIconColor);
+    }
+
+    /**
+     * Iteratively sets button colors for each button in {@link #mAllButtons}.
+     */
+    private void applyButtonColors(int iconColor) {
         for (ImageView button : mAllButtons) {
             button.setImageTintList(ColorStateList.valueOf(iconColor));
             Drawable background = button.getBackground();
             if (background instanceof KeyButtonRipple) {
                 ((KeyButtonRipple) background).setDarkIntensity(
-                        mTaskbarNavButtonDarkIntensity.value);
+                        getTaskbarNavButtonDarkIntensity().value);
             }
         }
     }
 
+    /**
+     * Updates Taskbar 3-Button icon colors as {@link #mTaskbarNavButtonDarkIntensity} changes.
+     */
     private void onDarkIntensityChanged() {
         updateNavButtonColor();
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
-            mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
+        if (mContext.isPhoneMode()) {
+            mTaskbarTransitions.onDarkIntensityChanged(getTaskbarNavButtonDarkIntensity().value);
         }
     }
 
@@ -783,12 +846,43 @@
         buttonView.setImageResource(drawableId);
         buttonView.setContentDescription(parent.getContext().getString(
                 navButtonController.getButtonContentDescription(buttonType)));
-        buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType, view));
-        buttonView.setOnLongClickListener(view ->
-                navButtonController.onButtonLongClick(buttonType, view));
+        if (predictiveBackThreeButtonNav() && buttonType == BUTTON_BACK) {
+            // set up special touch listener for back button to support predictive back
+            setBackButtonTouchListener(buttonView, navButtonController);
+        } else {
+            buttonView.setOnClickListener(view ->
+                    navButtonController.onButtonClick(buttonType, view));
+            buttonView.setOnLongClickListener(view ->
+                    navButtonController.onButtonLongClick(buttonType, view));
+        }
         return buttonView;
     }
 
+    private void setBackButtonTouchListener(View buttonView,
+            TaskbarNavButtonController navButtonController) {
+        buttonView.setOnTouchListener((v, event) -> {
+            if (event.getAction() == MotionEvent.ACTION_MOVE) return false;
+            long time = SystemClock.uptimeMillis();
+            int action = event.getAction();
+            KeyEvent keyEvent = new KeyEvent(time, time,
+                    action == MotionEvent.ACTION_DOWN ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
+                    KeyEvent.KEYCODE_BACK, 0);
+            if (event.getAction() == MotionEvent.ACTION_CANCEL) {
+                keyEvent.cancel();
+            }
+            navButtonController.executeBack(keyEvent);
+
+            if (action == MotionEvent.ACTION_UP) {
+                buttonView.performClick();
+            }
+            return false;
+        });
+        buttonView.setOnLongClickListener((view) ->  {
+            navButtonController.onButtonLongClick(BUTTON_BACK, view);
+            return false;
+        });
+    }
+
     private ImageView addButton(ViewGroup parent, @IdRes int id, @LayoutRes int layoutId) {
         ImageView buttonView = (ImageView) mContext.getLayoutInflater()
                 .inflate(layoutId, parent, false);
@@ -1115,7 +1209,7 @@
                 + mOnBackgroundNavButtonColorOverrideMultiplier.value);
 
         mNavButtonsView.dumpLogs(prefix + "\t", pw);
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.dumpLogs(prefix + "\t", pw);
         }
     }
@@ -1154,6 +1248,100 @@
         mHitboxExtender.onAnimationProgressToOverview(alignment);
     }
 
+    /** 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) {
+        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()) {
+            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
+            if (isNavbarOnRight) {
+                navBarTargetStartX = dp.widthPx - navBarSpacing - mNavButtonContainer.getWidth();
+            } else {
+                navBarTargetStartX = navBarSpacing;
+            }
+        } else {
+            // If the task bar is not start aligned, the navigation bar is located in the center
+            // between the taskbar and screen edges, depending on the bubble bar location.
+            float navbarWidth = mNavButtonContainer.getWidth();
+            Rect taskbarBounds = mControllers.taskbarViewController.getIconLayoutBounds();
+            if (isNavbarOnRight) {
+                if (mNavButtonsView.isLayoutRtl()) {
+                    float taskBarEnd = taskbarBounds.right;
+                    navBarTargetStartX = (dp.widthPx + taskBarEnd - navbarWidth) / 2;
+                } else {
+                    navBarTargetStartX = mNavButtonContainer.getLeft();
+                }
+            } else {
+                float taskBarStart = taskbarBounds.left;
+                navBarTargetStartX = (taskBarStart - navbarWidth) / 2;
+            }
+        }
+        return (int) navBarTargetStartX - mNavButtonContainer.getLeft();
+    }
+
+    /** Adjusts the navigation buttons layout position according to the bubble bar location. */
+    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()) {
+            if (mBubbleBarTargetLocation == null) {
+                // only set bubble bar location if it was not set before
+                mBubbleBarTargetLocation = mControllers.bubbleControllers.get()
+                        .bubbleBarViewController.getBubbleBarLocation();
+            }
+            onBubbleBarLocationUpdated(mBubbleBarTargetLocation);
+        }
+    }
+
     private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback {
         @Override
         public void onVisibilityChanged(boolean isVisible) {
@@ -1166,83 +1354,6 @@
         }
     }
 
-    private class RotationButtonImpl implements RotationButton {
-
-        private final ImageView mButton;
-        private AnimatedVectorDrawable mImageDrawable;
-
-        RotationButtonImpl(ImageView button) {
-            mButton = button;
-        }
-
-        @Override
-        public void setRotationButtonController(RotationButtonController rotationButtonController) {
-            // TODO(b/187754252) UI polish, different icons based on light/dark context, etc
-            mImageDrawable = (AnimatedVectorDrawable) mButton.getContext()
-                    .getDrawable(rotationButtonController.getIconResId());
-            mButton.setImageDrawable(mImageDrawable);
-            mButton.setContentDescription(mButton.getResources()
-                    .getString(R.string.accessibility_rotate_button));
-            mImageDrawable.setCallback(mButton);
-        }
-
-        @Override
-        public View getCurrentView() {
-            return mButton;
-        }
-
-        @Override
-        public boolean show() {
-            mButton.setVisibility(View.VISIBLE);
-            mState |= FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean hide() {
-            mButton.setVisibility(View.GONE);
-            mState &= ~FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean isVisible() {
-            return mButton.getVisibility() == View.VISIBLE;
-        }
-
-        @Override
-        public void updateIcon(int lightIconColor, int darkIconColor) {
-            // TODO(b/187754252): UI Polish
-        }
-
-        @Override
-        public void setOnClickListener(OnClickListener onClickListener) {
-            mButton.setOnClickListener(onClickListener);
-        }
-
-        @Override
-        public void setOnHoverListener(OnHoverListener onHoverListener) {
-            mButton.setOnHoverListener(onHoverListener);
-        }
-
-        @Override
-        public AnimatedVectorDrawable getImageDrawable() {
-            return mImageDrawable;
-        }
-
-        @Override
-        public void setDarkIntensity(float darkIntensity) {
-            // TODO(b/187754252) UI polish
-        }
-
-        @Override
-        public boolean acceptRotationProposal() {
-            return mButton.isAttachedToWindow();
-        }
-    }
-
     private static class StatePropertyHolder {
 
         private final float mEnabledValue, mDisabledValue;
@@ -1273,13 +1384,16 @@
             mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue);
         }
 
-        public void setState(int flags) {
+        public void setState(int flags, boolean skipAnimation) {
             boolean isEnabled = mEnableCondition.test(flags);
             if (mIsEnabled != isEnabled) {
                 mIsEnabled = isEnabled;
                 mAnimator.cancel();
                 mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue);
                 mAnimator.start();
+                if (skipAnimation) {
+                    mAnimator.end();
+                }
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.kt
new file mode 100644
index 0000000..dc66e0b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.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.taskbar
+
+import android.content.Context
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.view.View
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.R
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.popup.SystemShortcut
+import com.android.launcher3.views.ActivityContext
+
+/**
+ * A single menu item shortcut to execute creating a new instance of an app. Default interaction for
+ * [onClick] is to launch the app in full screen or as a floating window in Desktop Mode.
+ */
+class NewWindowTaskbarShortcut<T>(target: T, itemInfo: ItemInfo?, originalView: View?) :
+    SystemShortcut<T>(
+        R.drawable.desktop_mode_ic_taskbar_menu_new_window,
+        R.string.new_window_option_taskbar,
+        target,
+        itemInfo,
+        originalView
+    ) where T : Context?, T : ActivityContext? {
+
+    override fun onClick(v: View?) {
+        val intent = mItemInfo.intent ?: return
+        intent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK)
+        mTarget?.startActivitySafely(v, intent, mItemInfo)
+        AbstractFloatingView.closeAllOpenViews(mTarget)
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
index 94e2244..caf3320 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
@@ -47,6 +47,7 @@
     private final int[] mTmpArr = new int[2];
 
     private @Nullable ObjectAnimator mColorChangeAnim;
+    private Boolean mIsRegionDark;
 
     public StashedHandleView(Context context) {
         this(context, null);
@@ -95,7 +96,11 @@
      * @param animate Whether to animate the change, or apply it immediately.
      */
     public void updateHandleColor(boolean isRegionDark, boolean animate) {
+        if (mIsRegionDark != null && mIsRegionDark == isRegionDark) {
+            return;
+        }
         int newColor = isRegionDark ? mStashedHandleLightColor : mStashedHandleDarkColor;
+        mIsRegionDark = isRegionDark;
         if (mColorChangeAnim != null) {
             mColorChangeAnim.cancel();
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 252f2a8..eb47bb0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -41,8 +41,8 @@
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.NavHandle;
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.handles.RegionSamplingHelper;
 
 import java.io.PrintWriter;
 
@@ -94,6 +94,7 @@
     // States that affect whether region sampling is enabled or not
     private boolean mIsStashed;
     private boolean mIsLumaSamplingEnabled;
+    private boolean mIsAppTransitionPending;
     private boolean mTaskbarHidden;
 
     private float mTranslationYForSwipe;
@@ -191,7 +192,9 @@
 
 
     public void onDestroy() {
-        mRegionSamplingHelper.stopAndDestroy();
+        if (mRegionSamplingHelper != null) {
+            mRegionSamplingHelper.stopAndDestroy();
+        }
         mRegionSamplingHelper = null;
     }
 
@@ -209,7 +212,7 @@
      * morphs into the size of where the taskbar icons will be.
      */
     public Animator createRevealAnimToIsStashed(boolean isStashed) {
-        Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds());
+        Rect visualBounds = mControllers.taskbarViewController.getIconLayoutVisualBounds();
         float startRadius = mStashedHandleRadius;
 
         if (DisplayController.isTransientTaskbar(mActivity)) {
@@ -257,6 +260,11 @@
         updateSamplingState();
     }
 
+    public void setIsAppTransitionPending(boolean pending) {
+        mIsAppTransitionPending = pending;
+        updateSamplingState();
+    }
+
     private void updateSamplingState() {
         updateRegionSamplingWindowVisibility();
         if (shouldSample()) {
@@ -268,7 +276,7 @@
     }
 
     private boolean shouldSample() {
-        return mIsStashed && mIsLumaSamplingEnabled;
+        return mIsStashed && mIsLumaSamplingEnabled && !mIsAppTransitionPending;
     }
 
     protected void updateStashedHandleHintScale() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 78c6d4b..82acc0c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -25,6 +25,7 @@
 import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
@@ -36,6 +37,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN;
+import static com.android.launcher3.taskbar.TaskbarStashController.SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE;
 import static com.android.launcher3.testing.shared.ResourceUtils.getBoolByName;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
@@ -57,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;
@@ -68,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;
@@ -86,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;
@@ -99,19 +106,28 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSuspendFlag;
 import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
 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;
+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.BubbleStashController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider;
+import com.android.launcher3.taskbar.bubbles.stashing.DeviceProfileDimensionsProviderAdapter;
+import com.android.launcher3.taskbar.bubbles.stashing.PersistentBubbleStashController;
+import com.android.launcher3.taskbar.bubbles.stashing.TransientBubbleStashController;
+import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator;
+import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator;
 import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
 import com.android.launcher3.testing.TestLogging;
@@ -120,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;
@@ -132,12 +148,12 @@
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.ViewCache;
 import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.NavHandle;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
@@ -147,6 +163,7 @@
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.unfold.updates.RotationChangeProvider;
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
+import com.android.wm.shell.Flags;
 
 import java.io.PrintWriter;
 import java.util.Collections;
@@ -175,6 +192,7 @@
     private final WindowManager mWindowManager;
     private DeviceProfile mDeviceProfile;
     private WindowManager.LayoutParams mWindowLayoutParams;
+    private WindowManager.LayoutParams mLastUpdatedLayoutParams;
     private boolean mIsFullscreen;
     // The size we should return to when we call setTaskbarWindowFullscreen(false)
     private int mLastRequestedNonFullscreenSize;
@@ -205,15 +223,26 @@
 
     private final LauncherPrefs mLauncherPrefs;
 
+    private TaskbarFeatureEvaluator mTaskbarFeatureEvaluator;
+
+    private TaskbarSpecsEvaluator mTaskbarSpecsEvaluator;
+
     public TaskbarActivityContext(Context windowContext,
             @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp,
             TaskbarNavButtonController buttonController, ScopedUnfoldTransitionProgressProvider
-            unfoldTransitionProgressProvider) {
+            unfoldTransitionProgressProvider,
+            @NonNull DesktopVisibilityController desktopVisibilityController) {
         super(windowContext);
 
         mNavigationBarPanelContext = navigationBarPanelContext;
         applyDeviceProfile(launcherDp);
         final Resources resources = getResources();
+        mTaskbarFeatureEvaluator = TaskbarFeatureEvaluator.getInstance(this);
+        mTaskbarSpecsEvaluator = new TaskbarSpecsEvaluator(
+                this,
+                mTaskbarFeatureEvaluator,
+                mDeviceProfile.inv.numRows,
+                mDeviceProfile.inv.numColumns);
 
         mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, resources, false);
         mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
@@ -234,15 +263,20 @@
         mWindowManager = c.getSystemService(WindowManager.class);
 
         // Inflate views.
-        int taskbarLayout = DisplayController.isTransientTaskbar(this) && !isPhoneMode()
-                ? R.layout.transient_taskbar
-                : R.layout.taskbar;
+        final boolean isTransientTaskbar = DisplayController.isTransientTaskbar(this)
+                && !isPhoneMode();
+        int taskbarLayout = isTransientTaskbar ? R.layout.transient_taskbar : R.layout.taskbar;
         mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false);
         TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
         TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim);
         NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
         StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
-        BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
+        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);
 
         mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this);
@@ -250,18 +284,37 @@
         // 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 BubbleStashController(this),
-                    new BubbleStashedHandleViewController(this, bubbleHandleView),
+                    new BubbleBarViewController(this, bubbleBarView, bubbleBarContainer),
+                    bubbleStashController,
+                    bubbleHandleController,
                     new BubbleDragController(this),
                     new BubbleDismissController(this, mDragLayer),
                     new BubbleBarPinController(this, mDragLayer,
                             () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
                     new BubblePinController(this, mDragLayer,
-                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize)
+                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+                    bubbleBarSwipeController,
+                    new BubbleCreator(this)
             ));
         }
 
@@ -301,14 +354,12 @@
                 new VoiceInteractionWindowController(this),
                 new TaskbarTranslationController(this),
                 new TaskbarSpringOnStashController(this),
-                new TaskbarRecentAppsController(
-                        RecentsModel.INSTANCE.get(this),
-                        LauncherActivityInterface.INSTANCE::getDesktopVisibilityController),
+                new TaskbarRecentAppsController(this, RecentsModel.INSTANCE.get(this)),
                 TaskbarEduTooltipController.newInstance(this),
                 new KeyboardQuickSwitchController(),
-                new TaskbarPinningController(this, () ->
-                        DisplayController.isInDesktopMode(this)),
-                bubbleControllersOptional);
+                new TaskbarPinningController(this),
+                bubbleControllersOptional,
+                new TaskbarDesktopModeController(desktopVisibilityController));
 
         mLauncherPrefs = LauncherPrefs.get(this);
     }
@@ -316,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);
@@ -371,6 +425,7 @@
         mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false);
         mLastRequestedNonFullscreenSize = getDefaultTaskbarWindowSize();
         mWindowLayoutParams = createAllWindowParams();
+        mLastUpdatedLayoutParams = new WindowManager.LayoutParams();
 
         // Initialize controllers after all are constructed.
         mControllers.init(sharedState);
@@ -431,6 +486,15 @@
     }
 
     /**
+     * Returns {@code true} iff bubble bar is enabled (but not necessarily visible /
+     * containing bubbles).
+     */
+    @Override
+    public boolean isBubbleBarEnabled() {
+        return getBubbleControllers() != null && BubbleBarController.isBubbleBarEnabled();
+    }
+
+    /**
      * Returns if software keyboard is docked or input toolbar is placed at the taskbar area
      */
     public boolean isImeDocked() {
@@ -611,6 +675,11 @@
         return mNavMode == NavigationMode.THREE_BUTTONS;
     }
 
+    /** Returns whether taskbar should start align. */
+    public boolean shouldStartAlignTaskbar() {
+        return isThreeButtonNav() && mDeviceProfile.startAlignTaskbar;
+    }
+
     public boolean isGestureNav() {
         return mNavMode == NavigationMode.NO_BUTTON;
     }
@@ -795,12 +864,39 @@
         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
      */
     public void setUIController(@NonNull TaskbarUIController uiController) {
         mControllers.setUiController(uiController);
-        if (mControllers.bubbleControllers.isEmpty()) {
+        if (BubbleBarController.isBubbleBarEnabled() && mControllers.bubbleControllers.isEmpty()) {
             // if the bubble bar was visible in a previous configuration of taskbar and is being
             // recreated now without bubbles, clean up any bubble bar adjustments from hotseat
             bubbleBarVisibilityChanged(/* isVisible= */ false);
@@ -835,11 +931,16 @@
         mControllers.navbarButtonsViewController.transitionTo(barMode, animate);
     }
 
+    public void appTransitionPending(boolean pending) {
+        mControllers.stashedHandleViewController.setIsAppTransitionPending(pending);
+    }
+
     /**
      * Called when this instance of taskbar is no longer needed
      */
     public void onDestroy() {
         mIsDestroyed = true;
+        mTaskbarFeatureEvaluator.onDestroy();
         setUIController(TaskbarUIController.DEFAULT);
         mControllers.onDestroy();
         if (!enableTaskbarNoRecreate() && !ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
@@ -857,7 +958,7 @@
         mControllers.navbarButtonsViewController.updateStateForSysuiFlags(systemUiStateFlags,
                 fromInit);
         boolean isShadeVisible = (systemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0;
-        onNotificationShadeExpandChanged(isShadeVisible, fromInit);
+        onNotificationShadeExpandChanged(isShadeVisible, fromInit || isPhoneMode());
         mControllers.taskbarViewController.setRecentsButtonDisabled(
                 mControllers.navbarButtonsViewController.isRecentsDisabled()
                         || isNavBarKidsModeActive());
@@ -876,21 +977,34 @@
         mControllers.uiController.updateStateForSysuiFlags(systemUiStateFlags);
         mControllers.bubbleControllers.ifPresent(controllers -> {
             controllers.bubbleBarController.updateStateForSysuiFlags(systemUiStateFlags);
-            controllers.bubbleStashedHandleViewController.setIsHomeButtonDisabled(
-                    mControllers.navbarButtonsViewController.isHomeDisabled());
+            controllers.bubbleStashedHandleViewController.ifPresent(controller ->
+                    controller.setIsHomeButtonDisabled(
+                            mControllers.navbarButtonsViewController.isHomeDisabled()));
         });
     }
 
     /**
-     * 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();
@@ -915,9 +1029,10 @@
     public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
         mControllers.navbarButtonsViewController.onTransitionModeUpdated(barMode, checkBarModes);
     }
+
     public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
-        mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity()
-                .updateValue(darkIntensity);
+        mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity().updateValue(
+                darkIntensity);
     }
 
     public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
@@ -1064,6 +1179,9 @@
      * window.
      */
     public void setTaskbarWindowFocusable(boolean focusable) {
+        if (isPhoneMode()) {
+            return;
+        }
         if (focusable) {
             mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE;
         } else {
@@ -1076,7 +1194,7 @@
      * Applies forcibly show flag to taskbar window iff transient taskbar is unstashed.
      */
     public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) {
-        if (!DisplayController.isTransientTaskbar(this)) {
+        if (!DisplayController.isTransientTaskbar(this) || isPhoneMode()) {
             return;
         }
         if (shouldForceShow) {
@@ -1119,14 +1237,24 @@
         mControllers.uiController.startSplitSelection(splitSelectSource);
     }
 
+    boolean areDesktopTasksVisible() {
+        return mControllers != null
+                && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible();
+    }
+
     protected void onTaskbarIconClicked(View view) {
         TaskbarUIController taskbarUIController = mControllers.uiController;
         RecentsView recents = taskbarUIController.getRecentsView();
         boolean shouldCloseAllOpenViews = true;
         Object tag = view.getTag();
+
+        mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
+
         if (tag instanceof GroupTask groupTask) {
-            handleGroupTaskLaunch(groupTask, /* remoteTransition = */ null,
-                    DisplayController.isInDesktopMode(this));
+            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
@@ -1144,8 +1272,11 @@
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
         } else if (tag instanceof TaskItemInfo info) {
+            RemoteTransition remoteTransition = canUnminimizeDesktopTask(info.getTaskId())
+                    ? createUnminimizeRemoteTransition() : null;
             UI_HELPER_EXECUTOR.execute(() ->
-                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId()));
+                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(
+                            info.getTaskId(), remoteTransition));
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
                     /* stash= */ true);
         } else if (tag instanceof WorkspaceItemInfo) {
@@ -1160,7 +1291,8 @@
                     Intent intent = new Intent(info.getIntent())
                             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                     try {
-                        if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) {
+                        if (mIsSafeModeEnabled
+                                && !new ApplicationInfoWrapper(this, intent).isSystem()) {
                             Toast.makeText(this, R.string.safemode_shortcut_error,
                                     Toast.LENGTH_SHORT).show();
                         } else if (info.isPromise()) {
@@ -1235,7 +1367,8 @@
             GroupTask task,
             @Nullable RemoteTransition remoteTransition,
             boolean onDesktop) {
-        handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
+        handleGroupTaskLaunch(task, remoteTransition, onDesktop,
+                /* onStartCallback= */ null, /* onFinishCallback= */ null);
     }
 
     /**
@@ -1259,17 +1392,23 @@
             UI_HELPER_EXECUTOR.execute(() ->
                     SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
                             remoteTransition));
-        } else if (onDesktop) {
+            return;
+        }
+        if (onDesktop) {
+            boolean useRemoteTransition = canUnminimizeDesktopTask(task.task1.key.id);
             UI_HELPER_EXECUTOR.execute(() -> {
                 if (onStartCallback != null) {
                     onStartCallback.run();
                 }
-                SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id);
+                SystemUiProxy.INSTANCE.get(this).showDesktopApp(
+                        task.task1.key.id, useRemoteTransition ? remoteTransition : null);
                 if (onFinishCallback != null) {
                     onFinishCallback.run();
                 }
             });
-        } else if (task.task2 == null) {
+            return;
+        }
+        if (task.task2 == null) {
             UI_HELPER_EXECUTOR.execute(() -> {
                 ActivityOptions activityOptions =
                         makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
@@ -1278,9 +1417,23 @@
                 ActivityManagerWrapper.getInstance().startActivityFromRecents(
                         task.task1.key, activityOptions);
             });
-        } else {
-            mControllers.uiController.launchSplitTasks(task, remoteTransition);
+            return;
         }
+        mControllers.uiController.launchSplitTasks(task, remoteTransition);
+    }
+
+    /** Returns whether the given task is minimized and can be unminimized. */
+    public boolean canUnminimizeDesktopTask(int taskId) {
+        BubbleTextView.RunningAppState runningAppState =
+                mControllers.taskbarRecentAppsController.getRunningAppState(taskId);
+        return runningAppState == BubbleTextView.RunningAppState.MINIMIZED
+                && DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS.isTrue();
+    }
+
+    private RemoteTransition createUnminimizeRemoteTransition() {
+        return new RemoteTransition(
+                new DesktopAppLaunchTransition(
+                        this, getMainExecutor(), AppLaunchType.UNMINIMIZE));
     }
 
     /**
@@ -1343,10 +1496,11 @@
                     if (foundTask != null) {
                         TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
                         if (foundTaskView != null
-                                && foundTaskView.isVisibleToUser()) {
+                                && foundTaskView.isVisibleToUser()
+                                && !(foundTaskView instanceof DesktopTaskView)) {
                             TestLogging.recordEvent(
                                     TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
-                            foundTaskView.launchTasks();
+                            foundTaskView.launchWithAnimation();
                             return;
                         }
                     }
@@ -1380,26 +1534,31 @@
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         try {
             TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
-            if (info.user.equals(Process.myUserHandle())) {
-                // TODO(b/216683257): Use startActivityForResult for search results that require it.
-                if (taskInRecents != null) {
-                    // Re launch instance from recents
-                    ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
-                    opts.options.setLaunchDisplayId(
-                            getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
-                    if (ActivityManagerWrapper.getInstance()
-                            .startActivityFromRecents(taskInRecents.key, opts.options)) {
-                        mControllers.uiController.getRecentsView()
-                                .addSideTaskLaunchCallback(opts.onEndCallback);
-                        return;
-                    }
-                }
-
-                startActivity(intent);
-            } else {
+            if (!info.user.equals(Process.myUserHandle())) {
+                // TODO b/376819104: support Desktop launch animations for apps in managed profiles
                 getSystemService(LauncherApps.class).startMainActivity(
                         intent.getComponent(), info.user, intent.getSourceBounds(), null);
+                return;
             }
+            // TODO(b/216683257): Use startActivityForResult for search results that require it.
+            if (taskInRecents != null) {
+                // Re launch instance from recents
+                ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
+                opts.options.setLaunchDisplayId(
+                        getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
+                if (ActivityManagerWrapper.getInstance()
+                        .startActivityFromRecents(taskInRecents.key, opts.options)) {
+                    mControllers.uiController.getRecentsView()
+                            .addSideTaskLaunchCallback(opts.onEndCallback);
+                    return;
+                }
+            }
+            ActivityOptionsWrapper opts = null;
+            if (areDesktopTasksVisible()) {
+                opts = getActivityLaunchDesktopOptions(info);
+            }
+            Bundle optionsBundle = opts == null ? null : opts.options.toBundle();
+            startActivity(intent, optionsBundle);
         } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT)
                     .show();
@@ -1439,6 +1598,12 @@
                 itemView.setHapticFeedbackEnabled(true);
                 return false;
             });
+
+            // Close any open taskbar tooltips.
+            if (AbstractFloatingView.hasOpenView(this, TYPE_ON_BOARD_POPUP)) {
+                AbstractFloatingView.getOpenView(this, TYPE_ON_BOARD_POPUP)
+                        .close(/* animate= */ false);
+            }
         });
     }
 
@@ -1451,10 +1616,15 @@
 
     /**
      * 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() {
+    public void onSwipeToUnstashTaskbar(boolean delayTaskbarBackground) {
+        mControllers.uiController.onSwipeToUnstashTaskbar();
+
         boolean wasStashed = mControllers.taskbarStashController.isStashed();
-        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false);
+        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false,
+                SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE, delayTaskbarBackground);
         boolean isStashed = mControllers.taskbarStashController.isStashed();
         if (isStashed != wasStashed) {
             VibratorWrapper.INSTANCE.get(this).vibrateForTaskbarUnstash();
@@ -1588,7 +1758,7 @@
                 duration);
 
         View allAppsButton = mControllers.taskbarViewController.getAllAppsButtonView();
-        if (allAppsButton != null && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get()) {
+        if (!FeatureFlags.enableAllAppsButtonInHotseat()) {
             ValueAnimator alphaOverride = ValueAnimator.ofFloat(0, 1);
             alphaOverride.setDuration(duration);
             alphaOverride.addUpdateListener(a -> {
@@ -1607,7 +1777,7 @@
      * @param exclude {@code true} then the magnification region computation will omit the window.
      */
     public void excludeFromMagnificationRegion(boolean exclude) {
-        if (mIsExcludeFromMagnificationRegion == exclude) {
+        if (mIsExcludeFromMagnificationRegion == exclude || isPhoneMode()) {
             return;
         }
 
@@ -1624,6 +1794,12 @@
 
     void notifyUpdateLayoutParams() {
         if (mDragLayer.isAttachedToWindow()) {
+            // Copy the current windowLayoutParams to mLastUpdatedLayoutParams and compare the diff.
+            // If there is no change, we will skip the call to updateViewLayout.
+            int changes = mLastUpdatedLayoutParams.copyFrom(mWindowLayoutParams);
+            if (changes == 0) {
+                return;
+            }
             if (enableTaskbarNoRecreate()) {
                 mWindowManager.updateViewLayout(mDragLayer.getRootView(), mWindowLayoutParams);
             } else {
@@ -1649,6 +1825,14 @@
         return mControllers.taskbarStashController.isInStashedLauncherState();
     }
 
+    public TaskbarFeatureEvaluator getTaskbarFeatureEvaluator() {
+        return mTaskbarFeatureEvaluator;
+    }
+
+    public TaskbarSpecsEvaluator getTaskbarSpecsEvaluator() {
+        return mTaskbarSpecsEvaluator;
+    }
+
     protected void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarActivityContext:");
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index 2737cbd..c0e921e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -23,6 +23,7 @@
 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
@@ -66,8 +67,8 @@
     private var keyShadowDistance = 0f
     private var bottomMargin = 0
 
-    private val fullCornerRadius = context.cornerRadius.toFloat()
-    private var cornerRadius = fullCornerRadius
+    private val fullCornerRadius: Float
+    private var cornerRadius = 0f
     private var widthInsetPercentage = 0f
     private val square = Path()
     private val circle = Path()
@@ -97,7 +98,14 @@
             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
         }
 
-        setCornerRoundness(DEFAULT_ROUNDNESS)
+        if (context.areDesktopTasksVisible()) {
+            fullCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+            cornerRadius = fullCornerRadius
+        } else {
+            fullCornerRadius = context.cornerRadius.toFloat()
+            cornerRadius = fullCornerRadius
+            setCornerRoundness(MAX_ROUNDNESS)
+        }
     }
 
     fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) {
@@ -188,7 +196,7 @@
                 mapRange(
                         scale,
                         0f,
-                        res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat()
+                        res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat(),
                     )
                     .toInt()
             shadowBlur =
@@ -227,7 +235,7 @@
                 -mapRange(
                     1f - progress,
                     0f,
-                    if (isAnimatingPinning) 0f else stashedHandleHeight / 2f
+                    if (isAnimatingPinning) 0f else stashedHandleHeight / 2f,
                 )
 
         // Draw shadow.
@@ -237,7 +245,7 @@
             shadowBlur,
             0f,
             keyShadowDistance,
-            setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
+            setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)),
         )
         strokePaint.alpha = (paint.alpha * strokeAlpha) / 255
 
@@ -263,7 +271,7 @@
     }
 
     companion object {
-        const val DEFAULT_ROUNDNESS = 1f
+        const val MAX_ROUNDNESS = 1f
         private const val DARK_THEME_STROKE_ALPHA = 51
         private const val LIGHT_THEME_STROKE_ALPHA = 41
         private const val DARK_THEME_SHADOW_ALPHA = 51f
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 0645972..5a63ca6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -26,6 +26,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
 import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -64,6 +65,7 @@
     public final KeyboardQuickSwitchController keyboardQuickSwitchController;
     public final TaskbarPinningController taskbarPinningController;
     public final Optional<BubbleControllers> bubbleControllers;
+    public final TaskbarDesktopModeController taskbarDesktopModeController;
 
     @Nullable private LoggableTaskbarController[] mControllersToLog = null;
     @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
@@ -111,7 +113,8 @@
             TaskbarEduTooltipController taskbarEduTooltipController,
             KeyboardQuickSwitchController keyboardQuickSwitchController,
             TaskbarPinningController taskbarPinningController,
-            Optional<BubbleControllers> bubbleControllers) {
+            Optional<BubbleControllers> bubbleControllers,
+            TaskbarDesktopModeController taskbarDesktopModeController) {
         this.taskbarActivityContext = taskbarActivityContext;
         this.taskbarDragController = taskbarDragController;
         this.navButtonController = navButtonController;
@@ -138,6 +141,7 @@
         this.keyboardQuickSwitchController = keyboardQuickSwitchController;
         this.taskbarPinningController = taskbarPinningController;
         this.bubbleControllers = bubbleControllers;
+        this.taskbarDesktopModeController = taskbarDesktopModeController;
     }
 
     /**
@@ -165,6 +169,7 @@
         taskbarOverlayController.init(this);
         taskbarAllAppsController.init(this, sharedState.allAppsVisible);
         navButtonController.init(this);
+        bubbleControllers.ifPresent(controllers -> controllers.init(sharedState, this));
         taskbarInsetsController.init(this);
         voiceInteractionWindowController.init(this);
         taskbarRecentAppsController.init(this);
@@ -172,7 +177,7 @@
         taskbarEduTooltipController.init(this);
         keyboardQuickSwitchController.init(this);
         taskbarPinningController.init(this, mSharedState);
-        bubbleControllers.ifPresent(controllers -> controllers.init(this));
+        taskbarDesktopModeController.init(this, mSharedState);
 
         mControllersToLog = new LoggableTaskbarController[] {
                 taskbarDragController, navButtonController, navbarButtonsViewController,
@@ -188,8 +193,19 @@
                 taskbarDragLayerController, taskbarScrimViewController,
                 voiceInteractionWindowController
         };
-        mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS);
 
+        if (taskbarDesktopModeController.getAreDesktopTasksVisible()) {
+            mCornerRoundness.value = taskbarDesktopModeController.getTaskbarCornerRoundness(
+                    mSharedState.showCornerRadiusInDesktopMode);
+        } else {
+            mCornerRoundness.value = TaskbarBackgroundRenderer.MAX_ROUNDNESS;
+        }
+        updateCornerRoundness();
+        onPostInit();
+    }
+
+    @VisibleForTesting
+    public void onPostInit() {
         mAreAllControllersInitialized = true;
         for (Runnable postInitCallback : mPostInitCallbacks) {
             postInitCallback.run();
@@ -205,7 +221,12 @@
         uiController = newUiController;
         uiController.init(this);
         uiController.updateStateForSysuiFlags(mSharedState.sysuiStateFlags);
-
+        // if bubble controllers are present take bubble bar location, else set it to null
+        bubbleControllers.ifPresentOrElse(bubbleControllers -> {
+            BubbleBarLocation location =
+                    bubbleControllers.bubbleBarViewController.getBubbleBarLocation();
+            uiController.onBubbleBarLocationUpdated(location);
+        }, () -> uiController.onBubbleBarLocationUpdated(null));
         // Notify that the ui controller has changed
         navbarButtonsViewController.onUiControllerChanged();
     }
@@ -248,6 +269,7 @@
         keyboardQuickSwitchController.onDestroy();
         taskbarStashController.onDestroy();
         bubbleControllers.ifPresent(controllers -> controllers.onDestroy());
+        taskbarDesktopModeController.onDestroy();
 
         mControllersToLog = null;
         mBackgroundRendererControllers = null;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
new file mode 100644
index 0000000..47a35c5
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statehandlers.DesktopVisibilityController.TaskbarDesktopModeListener
+import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS
+
+/** Handles Taskbar in Desktop Windowing mode. */
+class TaskbarDesktopModeController(
+    private val desktopVisibilityController: DesktopVisibilityController
+) : TaskbarDesktopModeListener {
+    private lateinit var taskbarControllers: TaskbarControllers
+    private lateinit var taskbarSharedState: TaskbarSharedState
+
+    val areDesktopTasksVisible: Boolean
+        get() = desktopVisibilityController.areDesktopTasksVisible()
+
+    fun init(controllers: TaskbarControllers, sharedState: TaskbarSharedState) {
+        taskbarControllers = controllers
+        taskbarSharedState = sharedState
+        desktopVisibilityController.registerTaskbarDesktopModeListener(this)
+    }
+
+    override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {
+        taskbarSharedState.showCornerRadiusInDesktopMode = doesAnyTaskRequireTaskbarRounding
+        val cornerRadius = getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding)
+        taskbarControllers.taskbarCornerRoundness.animateToValue(cornerRadius).start()
+    }
+
+    fun getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding: Boolean): Float {
+        return if (doesAnyTaskRequireTaskbarRounding) {
+            MAX_ROUNDNESS
+        } else {
+            0f
+        }
+    }
+
+    fun onDestroy() = desktopVisibilityController.unregisterTaskbarDesktopModeListener(this)
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index a635537..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,12 +203,31 @@
 
     override fun addArrow() {
         super.addArrow()
-        // Change arrow location to the middle of popup.
-        mArrow.x = (dividerView.x + 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,
@@ -210,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()
     }
 
@@ -224,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)
@@ -240,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/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index efe42fb..fc307b2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -74,19 +74,17 @@
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.views.BubbleTextHolder;
-import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LogUtils;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.wm.shell.draganddrop.DragAndDropConstants;
+import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -344,12 +342,9 @@
     protected void callOnDragStart() {
         super.callOnDragStart();
         // TODO(297921594) clean it up when taskbar to desktop drag is implemented.
-        DesktopVisibilityController desktopController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-
         // Pre-drag has ended, start the global system drag.
-        if (mDisallowGlobalDrag || (desktopController != null
-                && desktopController.areDesktopTasksVisible())) {
+        if (mDisallowGlobalDrag
+                || mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) {
             AbstractFloatingView.closeAllOpenViewsExcept(mActivity, TYPE_TASKBAR_ALL_APPS);
             return;
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index a9b34d2..e16c76d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -99,7 +99,7 @@
     public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, 1 /* alphaChannelCount */);
-        mBackgroundRenderer = new TaskbarBackgroundRenderer(mActivity);
+        mBackgroundRenderer = new TaskbarBackgroundRenderer(mContainer);
 
         mTaskbarBackgroundAlpha = new MultiPropertyFactory<>(this, BG_ALPHA, INDEX_COUNT,
                 (a, b) -> a * b, 1f);
@@ -108,7 +108,7 @@
 
     public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) {
         mControllerCallbacks = callbacks;
-        mBackgroundRenderer.updateStashedHandleWidth(mActivity, getResources());
+        mBackgroundRenderer.updateStashedHandleWidth(mContainer, getResources());
         recreateControllers();
     }
 
@@ -269,7 +269,7 @@
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) {
-            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
             if (topView != null && topView.canHandleBack()) {
                 topView.onBackInvoked();
                 // Handled by the floating view.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index ff890fb..2845cee 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -171,6 +171,10 @@
     }
 
     private void updateBackgroundAlpha() {
+        if (mActivity.isPhoneMode()) {
+            return;
+        }
+
         final float bgNavbar = mBgNavbar.value;
         final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value
                 * mNotificationShadeBgTaskbar.value * mImeBgTaskbar.value
@@ -291,7 +295,7 @@
             if (mActivity.isPhoneMode()) {
                 Resources resources = mActivity.getResources();
                 Point taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile,
-                        resources, true /* isPhoneMode */);
+                        resources, true /* isPhoneMode */, mActivity.isGestureNav());
                 return taskbarDimensions.y == -1 ?
                         deviceProfile.getDisplayInfo().currentSize.y :
                         taskbarDimensions.y;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index 7f9d8a3..19e9872 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -53,8 +53,7 @@
 
     private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
 
-    private val backgroundColor =
-        Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright)
+    private val backgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright)
 
     private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index d57c483..a89bc3a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -33,6 +33,7 @@
 import android.widget.TextView
 import androidx.annotation.IntDef
 import androidx.annotation.LayoutRes
+import androidx.annotation.VisibleForTesting
 import androidx.core.text.HtmlCompat
 import androidx.core.view.updateLayoutParams
 import com.airbnb.lottie.LottieAnimationView
@@ -48,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
 
@@ -79,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() &&
@@ -87,7 +93,7 @@
                 !activityContext.isTinyTaskbar
         }
 
-    private val isOpen: Boolean
+    val isTooltipOpen: Boolean
         get() = tooltip?.isOpen ?: false
 
     val isBeforeTooltipFeaturesStep: Boolean
@@ -96,7 +102,8 @@
     private lateinit var controllers: TaskbarControllers
 
     // Keep track of whether the user has seen the Search Edu
-    private var userHasSeenSearchEdu: Boolean
+    @VisibleForTesting
+    var userHasSeenSearchEdu: Boolean
         get() {
             return TASKBAR_SEARCH_EDU_SEEN.get(activityContext)
         }
@@ -258,7 +265,8 @@
                 !DisplayController.isPinnedTaskbar(activityContext) ||
                 !isTooltipEnabled ||
                 !shouldShowSearchEdu ||
-                userHasSeenSearchEdu
+                userHasSeenSearchEdu ||
+                !controllers.taskbarStashController.isTaskbarVisibleAndNotStashing
         ) {
             return
         }
@@ -349,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)
         }
@@ -376,7 +384,7 @@
             override fun performAccessibilityAction(
                 host: View,
                 action: Int,
-                args: Bundle?
+                args: Bundle?,
             ): Boolean {
                 if (action == R.id.close) {
                     hide()
@@ -394,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),
                     )
                 )
             }
@@ -409,7 +417,7 @@
     override fun dumpLogs(prefix: String?, pw: PrintWriter?) {
         pw?.println(prefix + "TaskbarEduTooltipController:")
         pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled")
-        pw?.println("$prefix\tisOpen=$isOpen")
+        pw?.println("$prefix\tisOpen=$isTooltipOpen")
         pw?.println("$prefix\ttooltipStep=$tooltipStep")
     }
 
@@ -419,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 6ac862e..b7f5575 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
@@ -85,6 +85,9 @@
 
     /** Update values tracked via sysui flags. */
     public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) {
+        if (mContext.isPhoneMode()) {
+            return;
+        }
         mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0;
         if (mContext.isNavBarForceVisible()) {
             if (mIsImmersiveMode) {
@@ -105,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/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index dd14109..3bff31f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -18,30 +18,22 @@
 import static android.view.MotionEvent.ACTION_HOVER_ENTER;
 import static android.view.MotionEvent.ACTION_HOVER_EXIT;
 import static android.view.View.ALPHA;
-import static android.view.View.SCALE_Y;
-import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT;
 
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_EXCEPT_ON_BOARD_POPUP;
+import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
-import static com.android.launcher3.views.ArrowTipView.TEXT_ALPHA;
 
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
 import android.view.ContextThemeWrapper;
 import android.view.MotionEvent;
 import android.view.View;
 
-import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.apppairs.AppPairIcon;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.views.ArrowTipView;
 
@@ -49,19 +41,16 @@
  * Controls showing a tooltip in the taskbar above each icon when it is hovered.
  */
 public class TaskbarHoverToolTipController implements View.OnHoverListener {
-
-    private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 250;
-    private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150;
-
-    private final Handler mHoverToolTipHandler = new Handler(Looper.getMainLooper());
-    private final Runnable mRevealHoverToolTipRunnable = this::revealHoverToolTip;
-    private final Runnable mHideHoverToolTipRunnable = this::hideHoverToolTip;
+    // Short duration to reveal tooltip, as it is positioned in the x/y via a post() call in
+    // parallel with the open animation. An instant animation could show in the wrong location.
+    private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 15;
 
     private final TaskbarActivityContext mActivity;
     private final TaskbarView mTaskbarView;
     private final View mHoverView;
     private final ArrowTipView mHoverToolTipView;
     private final String mToolTipText;
+    private final int mYOffset;
 
     public TaskbarHoverToolTipController(TaskbarActivityContext activity, TaskbarView taskbarView,
             View hoverView) {
@@ -90,69 +79,36 @@
                 R.dimen.taskbar_tooltip_horizontal_padding);
         mHoverToolTipView.findViewById(R.id.text).setPadding(horizontalPadding, verticalPadding,
                 horizontalPadding, verticalPadding);
-
-        AnimatorSet hoverCloseAnimator = new AnimatorSet();
-        ObjectAnimator textCloseAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0);
-        textCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0, 0.33f));
-        ObjectAnimator alphaCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0);
-        alphaCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 0.66f));
-        ObjectAnimator scaleCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0);
-        scaleCloseAnimator.setInterpolator(Interpolators.STANDARD);
-        hoverCloseAnimator.playTogether(
-                textCloseAnimator,
-                alphaCloseAnimator,
-                scaleCloseAnimator);
-        hoverCloseAnimator.setStartDelay(0);
-        hoverCloseAnimator.setDuration(HOVER_TOOL_TIP_EXIT_DURATION);
-        mHoverToolTipView.setCustomCloseAnimation(hoverCloseAnimator);
+        mHoverToolTipView.setAlpha(0);
+        mYOffset = arrowContextWrapper.getResources().getDimensionPixelSize(
+                R.dimen.taskbar_tooltip_y_offset);
 
         AnimatorSet hoverOpenAnimator = new AnimatorSet();
-        ObjectAnimator textOpenAnimator =
-                ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0, 255);
-        textOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.15f, 0.75f));
-        ObjectAnimator scaleOpenAnimator =
-                ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0f, 1f);
-        scaleOpenAnimator.setInterpolator(Interpolators.EMPHASIZED);
         ObjectAnimator alphaOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0f, 1f);
-        alphaOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0f, 0.33f));
-        hoverOpenAnimator.playTogether(
-                scaleOpenAnimator,
-                textOpenAnimator,
-                alphaOpenAnimator);
+        hoverOpenAnimator.play(alphaOpenAnimator);
         hoverOpenAnimator.setDuration(HOVER_TOOL_TIP_REVEAL_DURATION);
         mHoverToolTipView.setCustomOpenAnimation(hoverOpenAnimator);
 
         mHoverToolTipView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     mHoverToolTipView.setPivotY(bottom);
-                    mHoverToolTipView.setY(mTaskbarView.getTop() - (bottom - top));
+                    mHoverToolTipView.setY(mTaskbarView.getTop() - mYOffset - (bottom - top));
                 });
     }
 
     @Override
     public boolean onHover(View v, MotionEvent event) {
-        boolean isAnyOtherFloatingViewOpen =
-                AbstractFloatingView.hasOpenView(mActivity, TYPE_ALL_EXCEPT_ON_BOARD_POPUP);
-        if (isAnyOtherFloatingViewOpen) {
-            mHoverToolTipHandler.removeCallbacksAndMessages(null);
-        }
+        boolean isFolderOpen = AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER);
         // If hover leaves a taskbar icon animate the tooltip closed.
         if (event.getAction() == ACTION_HOVER_EXIT) {
-            startHideHoverToolTip();
+            mHoverToolTipView.close(/* animate= */ false);
             mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false);
-            return true;
-        } else if (!isAnyOtherFloatingViewOpen && event.getAction() == ACTION_HOVER_ENTER) {
-            // If hovering above a taskbar icon starts, animate the tooltip open. Do not
-            // reveal if any floating views such as folders or edu pop-ups are open.
-            startRevealHoverToolTip();
+        } else if (!isFolderOpen && event.getAction() == ACTION_HOVER_ENTER) {
+            // Do not reveal if any floating views such as folders or edu pop-ups are open.
+            revealHoverToolTip();
             mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true);
-            return true;
         }
-        return false;
-    }
-
-    private void startRevealHoverToolTip() {
-        mHoverToolTipHandler.post(mRevealHoverToolTipRunnable);
+        return true;
     }
 
     private void revealHoverToolTip() {
@@ -168,16 +124,6 @@
         }
         Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
         mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
-                mTaskbarView.getTop(), /* shouldAutoClose= */ false);
-    }
-
-    private void startHideHoverToolTip() {
-        int accessibilityHideTimeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(
-                mActivity, /* originalTimeout= */ 0, FLAG_CONTENT_TEXT);
-        mHoverToolTipHandler.postDelayed(mHideHoverToolTipRunnable, accessibilityHideTimeout);
-    }
-
-    private void hideHoverToolTip() {
-        mHoverToolTipView.close(/* animate = */ true);
+                mTaskbarView.getTop() - mYOffset, /* shouldAutoClose= */ false);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 2103ebb..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
@@ -56,7 +55,6 @@
 import com.android.launcher3.util.Executors
 import java.io.PrintWriter
 import kotlin.jvm.optionals.getOrNull
-import kotlin.math.max
 
 /** Handles the insets that Taskbar provides to underlying apps and the IME. */
 class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController {
@@ -64,6 +62,10 @@
     companion object {
         private const val INDEX_LEFT = 0
         private const val INDEX_RIGHT = 1
+
+        private fun Region.addBoundsToRegion(bounds: Rect?) {
+            bounds?.let { op(it, Region.Op.UNION) }
+        }
     }
 
     /** The bottom insets taskbar provides to the IME when IME is visible. */
@@ -79,7 +81,7 @@
             context.mainThreadHandler,
             Executors.UI_HELPER_EXECUTOR.handler,
             context,
-            this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged
+            this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged,
         )
     private val debugTouchableRegion = DebugTouchableRegion()
 
@@ -102,7 +104,8 @@
     }
 
     fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() {
-        val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
+        val taskbarStashController = controllers.taskbarStashController
+        val tappableHeight = taskbarStashController.tappableHeightToReportToApps
         // We only report tappableElement height for unstashed, persistent taskbar,
         // which is also when we draw the rounded corners above taskbar.
         val insetsRoundedCornerFlag =
@@ -116,7 +119,7 @@
             if (enableTaskbarNoRecreate() && controllers.sharedState != null) {
                 getProvidedInsets(
                     controllers.sharedState!!.insetsFrameProviders,
-                    insetsRoundedCornerFlag
+                    insetsRoundedCornerFlag,
                 )
             } else {
                 getProvidedInsets(insetsRoundedCornerFlag)
@@ -128,47 +131,32 @@
             }
         }
 
-        val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight
+        val bubbleControllers = controllers.bubbleControllers.getOrNull()
+        val taskbarTouchableHeight = taskbarStashController.touchableHeight
         val bubblesTouchableHeight =
-            if (controllers.bubbleControllers.isPresent) {
-                controllers.bubbleControllers.get().bubbleStashController.touchableHeight
-            } else {
-                0
+            bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0
+        // reset touch bounds
+        defaultTouchableRegion.setEmpty()
+        if (bubbleControllers != null) {
+            val bubbleBarViewController = bubbleControllers.bubbleBarViewController
+            val isBubbleBarVisible = bubbleControllers.bubbleStashController.isBubbleBarVisible()
+            val isAnimatingNewBubble = bubbleBarViewController.isAnimatingNewBubble
+            // 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)
             }
-        val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight)
-
+        }
         if (
-            controllers.bubbleControllers.isPresent &&
-                controllers.bubbleControllers.get().bubbleStashController.isBubblesShowingOnHome
+            taskbarStashController.isInApp ||
+                controllers.uiController.isInOverviewUi ||
+                DisplayController.showLockedTaskbarOnHome(context)
         ) {
-            val iconBounds =
-                controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
-            defaultTouchableRegion.set(
-                iconBounds.left,
-                iconBounds.top,
-                iconBounds.right,
-                iconBounds.bottom
-            )
-        } else {
-            defaultTouchableRegion.set(
-                0,
-                windowLayoutParams.height - touchableHeight,
-                context.deviceProfile.widthPx,
-                windowLayoutParams.height
-            )
-
-            // if there's an animating bubble add it to the touch region so that it's clickable
-            val isAnimatingNewBubble =
-                controllers.bubbleControllers
-                    .getOrNull()
-                    ?.bubbleBarViewController
-                    ?.isAnimatingNewBubble
-                    ?: false
-            if (isAnimatingNewBubble) {
-                val iconBounds =
-                    controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
-                defaultTouchableRegion.op(iconBounds, Region.Op.UNION)
-            }
+            // only add the taskbar touch region if not on home
+            val bottom = windowLayoutParams.height
+            val top = bottom - taskbarTouchableHeight
+            val right = context.deviceProfile.widthPx
+            defaultTouchableRegion.addBoundsToRegion(Rect(/* left= */ 0, top, right, bottom))
         }
 
         // Pre-calculate insets for different providers across different rotations for this gravity
@@ -193,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
@@ -219,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),
         )
     }
 
@@ -238,20 +226,20 @@
             provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, endRotation)
         } else if (provider.type == mandatorySystemGestures()) {
             if (context.isThreeButtonNav) {
-                provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity,
-                    endRotation)
+                provider.insetsSize =
+                    getInsetsForGravityWithCutout(contentHeight, gravity, endRotation)
             } else {
                 val gestureHeight =
-                        ResourceUtils.getNavbarSize(
+                    ResourceUtils.getNavbarSize(
                         ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
-                        context.resources)
-                val isPinnedTaskbar = context.deviceProfile.isTaskbarPresent
-                        && !context.deviceProfile.isTransientTaskbar
-                val mandatoryGestureHeight =
-                        if (isPinnedTaskbar) contentHeight
-                        else gestureHeight
-                provider.insetsSize = getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity,
-                        endRotation)
+                        context.resources,
+                    )
+                val isPinnedTaskbar =
+                    context.deviceProfile.isTaskbarPresent &&
+                        !context.deviceProfile.isTransientTaskbar
+                val mandatoryGestureHeight = if (isPinnedTaskbar) contentHeight else gestureHeight
+                provider.insetsSize =
+                    getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, endRotation)
             }
         } else if (provider.type == tappableElement()) {
             provider.insetsSize = getInsetsForGravity(tappableHeight, gravity)
@@ -270,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)
@@ -284,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 =
@@ -296,7 +284,7 @@
                 InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
                 InsetsFrameProvider.InsetsSizeOverride(
                     TYPE_VOICE_INTERACTION,
-                    visInsetsSizeForTappableElement
+                    visInsetsSizeForTappableElement,
                 ),
             )
         if (
@@ -358,13 +346,6 @@
      */
     fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) {
         insetsInfo.touchableRegion.setEmpty()
-        // Always have nav buttons be touchable
-        controllers.navbarButtonsViewController.addVisibleButtonsRegion(
-            context.dragLayer,
-            insetsInfo.touchableRegion
-        )
-        debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
-
         val bubbleBarVisible =
             controllers.bubbleControllers.isPresent &&
                 controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
@@ -426,7 +407,7 @@
 
                 // Include the bounds of the bubble bar in the touchable region if they exist.
                 if (bubbleBarBounds != null) {
-                    region.op(bubbleBarBounds, Region.Op.UNION)
+                    region.addBoundsToRegion(bubbleBarBounds)
                 }
                 insetsInfo.touchableRegion.set(region)
                 debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview"
@@ -443,6 +424,12 @@
             debugTouchableRegion.lastSetTouchableReason =
                 "Icons are not visible, but other components such as 3 buttons might be"
         }
+        // Always have nav buttons be touchable
+        controllers.navbarButtonsViewController.addVisibleButtonsRegion(
+            context.dragLayer,
+            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 cb9f24a..fa04739 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -16,16 +16,24 @@
 package com.android.launcher3.taskbar;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
+import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT;
+import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
+import static com.android.launcher3.Utilities.isRtl;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
+import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_FOR_BUBBLES;
+import static com.android.launcher3.taskbar.TaskbarStashController.UNLOCK_TRANSITION_MEMOIZATION_MS;
 import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
+import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS;
+import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS;
 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
+import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_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;
@@ -33,12 +41,16 @@
 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;
 import com.android.launcher3.Utilities;
@@ -46,15 +58,19 @@
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
 import com.android.quickstep.util.SystemUiFlagUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.animation.ViewRootSync;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -143,16 +159,23 @@
     private AnimatedFloat mTaskbarBackgroundAlpha;
     private AnimatedFloat mTaskbarAlpha;
     private AnimatedFloat mTaskbarCornerRoundness;
-    private MultiProperty mIconAlphaForHome;
+    private MultiProperty mTaskbarAlphaForHome;
+    private @Nullable Animator mHotseatTranslationXAnimation;
     private QuickstepLauncher mLauncher;
 
+    private boolean mIsDestroyed = false;
     private Integer mPrevState;
     private int mState;
     private LauncherState mLauncherState = LauncherState.NORMAL;
     private boolean mSkipNextRecentsAnimEnd;
 
     // Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime (milliseconds).
-    private long mLastUnlockTimeMs = 0;
+    private long mLastRemoveTaskbarHiddenTimeMs = 0;
+    /**
+     * Time when FLAG_DEVICE_LOCKED was last cleared, plus
+     * {@link TaskbarStashController#UNLOCK_TRANSITION_MEMOIZATION_MS}
+     */
+    private long mLastUnlockTransitionTimeout;
 
     private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener;
 
@@ -160,6 +183,8 @@
 
     private boolean mShouldDelayLauncherStateAnim;
 
+    private @Nullable BubbleBarLocation mBubbleBarLocation;
+
     // We skip any view synchronizations during init/destroy.
     private boolean mCanSyncViews;
 
@@ -172,11 +197,13 @@
                     if (mIsQsbInline && !dp.isQsbInline) {
                         // We only modify QSB alpha if isQsbInline = true. If we switch to a DP
                         // where isQsbInline = false, then we need to reset the alpha.
-                        mLauncher.getHotseat().setQsbAlpha(1f);
+                        mLauncher.getHotseat().setQsbAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
                     }
                     mIsQsbInline = dp.isQsbInline;
                     TaskbarLauncherStateController.this.updateIconAlphaForHome(
-                            mIconAlphaForHome.getValue());
+                            mTaskbarAlphaForHome.getValue(), ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
+                    TaskbarLauncherStateController.this.onBubbleBarLocationChanged(
+                            mBubbleBarLocation, /* animate = */ false);
                 }
             };
 
@@ -239,31 +266,34 @@
                 .getTaskbarBackgroundAlpha();
         mTaskbarAlpha = mControllers.taskbarDragLayerController.getTaskbarAlpha();
         mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness();
-        mIconAlphaForHome = mControllers.taskbarViewController
+        mTaskbarAlphaForHome = mControllers.taskbarViewController
                 .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME);
 
         resetIconAlignment();
 
-        mLauncher.getStateManager().addStateListener(mStateListener);
+        if (!mControllers.taskbarActivityContext.isPhoneMode()) {
+            mLauncher.getStateManager().addStateListener(mStateListener);
+        }
         mLauncherState = launcher.getStateManager().getState();
         updateStateForSysuiFlags(sysuiStateFlags, /*applyState*/ false);
 
         applyState(0);
 
-        mCanSyncViews = true;
+        mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode();
         mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
         updateOverviewDragState(mLauncherState);
     }
 
     public void onDestroy() {
+        mIsDestroyed = true;
         mCanSyncViews = false;
 
         mIconAlignment.finishAnimation();
 
-        mLauncher.getHotseat().setIconsAlpha(1f);
+        mLauncher.getHotseat().setIconsAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
         mLauncher.getStateManager().removeStateListener(mStateListener);
 
-        mCanSyncViews = true;
+        mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode();
         mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
     }
 
@@ -289,6 +319,7 @@
         stashController.updateStateForFlag(FLAG_IN_APP, false);
 
         updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, true);
+        mLauncherState = toState;
         animatorSet.play(stashController.createApplyStateAnimator(duration));
         animatorSet.play(applyState(duration, false));
 
@@ -345,14 +376,7 @@
 
         updateStateForFlag(FLAG_DEVICE_LOCKED, SystemUiFlagUtils.isLocked(systemUiStateFlags));
 
-        // Taskbar is hidden whenever the device is dreaming. The dreaming state includes the
-        // interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in
-        // when the device is asleep, the second condition extends ensures that the transition from
-        // and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar
-        // hide/reveal animation timings.
-        boolean isTaskbarHidden = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING)
-                || (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE;
-        updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden);
+        updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
 
         if (applyState) {
             applyState();
@@ -367,7 +391,7 @@
     private void updateOverviewDragState(LauncherState launcherState) {
         boolean disallowLongClick =
                 FeatureFlags.enableSplitContextually()
-                        ? mLauncher.isSplitSelectionActive()
+                        ? mLauncher.isSplitSelectionActive() || mIsAnimatingToLauncher
                         : launcherState == LauncherState.OVERVIEW_SPLIT_SELECT;
         com.android.launcher3.taskbar.Utilities.setOverviewDragState(
                 mControllers, launcherState.disallowTaskbarGlobalDrag(),
@@ -407,7 +431,7 @@
     }
 
     public Animator applyState(long duration, boolean start) {
-        if (mControllers.taskbarActivityContext.isDestroyed()) {
+        if (mIsDestroyed || mControllers.taskbarActivityContext.isPhoneMode()) {
             return null;
         }
         Animator animator = null;
@@ -444,14 +468,20 @@
                     + ", toAlignment: " + toAlignment);
         }
         mControllers.bubbleControllers.ifPresent(controllers -> {
-            // Show the bubble bar when on launcher home or in overview.
-            boolean onHome = isInLauncher && mLauncherState == LauncherState.NORMAL;
+            // Show the bubble bar when on launcher home (hotseat icons visible) or in overview
             boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
-            controllers.bubbleStashController.setBubblesShowingOnHome(onHome);
-            controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview);
+            boolean hotseatIconsVisible = isInLauncher && mLauncherState.areElementsVisible(
+                    mLauncher, HOTSEAT_ICONS);
+            BubbleLauncherState state = onOverview
+                    ? BubbleLauncherState.OVERVIEW
+                    : hotseatIconsVisible
+                            ? BubbleLauncherState.HOME
+                            : BubbleLauncherState.IN_APP;
+            controllers.bubbleStashController.setLauncherState(state);
         });
 
-        mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_OVERVIEW,
+        TaskbarStashController stashController = mControllers.taskbarStashController;
+        stashController.updateStateForFlag(FLAG_IN_OVERVIEW,
                 mLauncherState == LauncherState.OVERVIEW);
 
         AnimatorSet animatorSet = new AnimatorSet();
@@ -470,7 +500,8 @@
                 // We're changing state to home, should close open popups e.g. Taskbar AllApps
                 handleOpenFloatingViews = true;
             }
-            if (mLauncherState == LauncherState.OVERVIEW) {
+            if (mLauncherState == LauncherState.OVERVIEW
+                    && !mControllers.taskbarActivityContext.isPhoneMode()) {
                 // Calling to update the insets in TaskbarInsetController#updateInsetsTouchability
                 mControllers.taskbarActivityContext.notifyUpdateLayoutParams();
             }
@@ -482,8 +513,6 @@
                 public void onAnimationStart(Animator animation) {
                     mIsAnimatingToLauncher = isInLauncher;
 
-                    TaskbarStashController stashController =
-                            mControllers.taskbarStashController;
                     if (DEBUG) {
                         Log.d(TAG, "onAnimationStart - FLAG_IN_APP: " + !isInLauncher);
                     }
@@ -499,6 +528,8 @@
 
             // Handle closing open popups when going home/overview
             handleOpenFloatingViews = true;
+        } else {
+            stashController.applyState();
         }
 
         if (handleOpenFloatingViews && isInLauncher) {
@@ -507,7 +538,7 @@
 
         if (hasAnyFlag(changedFlags, FLAG_TASKBAR_HIDDEN) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
             // Take note of the current time, as the taskbar is made visible again.
-            mLastUnlockTimeMs = SystemClock.elapsedRealtime();
+            mLastRemoveTaskbarHiddenTimeMs = SystemClock.elapsedRealtime();
         }
 
         boolean isHidden = hasAnyFlag(FLAG_TASKBAR_HIDDEN);
@@ -533,7 +564,8 @@
                 // with a fingerprint reader. This should only be done when the device was woken
                 // up via fingerprint reader, however since this information is currently not
                 // available, opting to always delay the fade-in a bit.
-                long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() - mLastUnlockTimeMs;
+                long durationSinceLastUnlockMs = SystemClock.elapsedRealtime()
+                        - mLastRemoveTaskbarHiddenTimeMs;
                 taskbarVisibility.setStartDelay(
                         Math.max(0, TASKBAR_SHOW_DELAY_MS - durationSinceLastUnlockMs));
             }
@@ -579,6 +611,13 @@
 
         float cornerRoundness = isInLauncher ? 0 : 1;
 
+        if (mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()
+                && mControllers.getSharedState() != null) {
+            cornerRoundness =
+                    mControllers.taskbarDesktopModeController.getTaskbarCornerRoundness(
+                            mControllers.getSharedState().showCornerRadiusInDesktopMode);
+        }
+
         // Don't animate if corner roundness has reached desired value.
         if (mTaskbarCornerRoundness.isAnimating()
                 || mTaskbarCornerRoundness.value != cornerRoundness) {
@@ -596,6 +635,15 @@
         boolean isUnlockTransition =
                 hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED) && !hasAnyFlag(FLAG_DEVICE_LOCKED);
         if (isUnlockTransition) {
+            // the launcher might not be resumed at the time the device is considered
+            // unlocked (when the keyguard goes away), but possibly shortly afterwards.
+            // To play the unlock transition at the time the unstash animation actually happens,
+            // this memoizes the state transition for UNLOCK_TRANSITION_MEMOIZATION_MS.
+            mLastUnlockTransitionTimeout =
+                    SystemClock.elapsedRealtime() + UNLOCK_TRANSITION_MEMOIZATION_MS;
+        }
+        boolean isInUnlockTimeout = SystemClock.elapsedRealtime() < mLastUnlockTransitionTimeout;
+        if (isUnlockTransition || isInUnlockTimeout) {
             // When transitioning to unlocked, ensure the hotseat is fully visible from the
             // beginning. The hotseat itself is animated by LauncherUnlockAnimationController.
             mIconAlignment.cancelAnimation();
@@ -623,10 +671,16 @@
                         + mIconAlignment.value
                         + " -> " + toAlignment + ": " + duration);
             }
-            animatorSet.play(iconAlignAnim);
+            if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
+                iconAlignAnim.setInterpolator(FINAL_FRAME);
+            } else {
+                animatorSet.play(iconAlignAnim);
+            }
         }
 
-        animatorSet.setInterpolator(EMPHASIZED);
+        Interpolator interpolator = enableScalingRevealHomeAnimation()
+                ? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED;
+        animatorSet.setInterpolator(interpolator);
 
         if (start) {
             animatorSet.start();
@@ -640,6 +694,9 @@
      * This refers to the intended state - a transition to this state might be in progress.
      */
     public boolean isTaskbarAlignedWithHotseat() {
+        if (DisplayController.showLockedTaskbarOnHome(mLauncher) && isInLauncher()) {
+            return false;
+        }
         return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
     }
 
@@ -651,8 +708,7 @@
             boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
             boolean willStashVisually = isInStashedState
                     && mControllers.taskbarStashController.supportsVisualStashing();
-            boolean isTaskbarAlignedWithHotseat =
-                    mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
+            boolean isTaskbarAlignedWithHotseat = isTaskbarAlignedWithHotseat();
             return isTaskbarAlignedWithHotseat && !willStashVisually;
         } else {
             return false;
@@ -683,14 +739,17 @@
                 public void onAnimationEnd(Animator animation) {
                     if (isInStashedState && committed) {
                         // Reset hotseat alpha to default
-                        mLauncher.getHotseat().setIconsAlpha(1);
+                        mLauncher.getHotseat().setIconsAlpha(1, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
                     }
                 }
 
                 @Override
                 public void onAnimationStart(Animator animation) {
-                    if (mLauncher.getHotseat().getIconsAlpha() > 0) {
-                        updateIconAlphaForHome(mLauncher.getHotseat().getIconsAlpha());
+                    float hotseatIconsAlpha = mLauncher.getHotseat()
+                            .getIconsAlpha(ALPHA_CHANNEL_TASKBAR_ALIGNMENT)
+                            .getValue();
+                    if (hotseatIconsAlpha > 0) {
+                        updateIconAlphaForHome(hotseatIconsAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
                     }
                 }
             });
@@ -719,6 +778,36 @@
         }
     }
 
+    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() {
+            @Override
+            public void run() {
+                updateIconAlphaForHome(stash ? 1 : 0, ALPHA_CHANNEL_TASKBAR_STASH);
+            }
+        };
+        if (stash) {
+            stashController.applyState();
+            // if we stashing the hotseat we need to immediately swap it with the animating taskbar
+            swapHotseatWithTaskbar.run();
+        } else {
+            // if we revert stashing make swap after taskbar animation is complete
+            stashController.applyState(/* postApplyAction = */ swapHotseatWithTaskbar);
+        }
+    }
+
+    protected void unStashHotseatInstantly() {
+        TaskbarStashController stashController = mControllers.taskbarStashController;
+        stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, false);
+        stashController.applyState(/* duration = */ 0);
+        updateIconAlphaForHome(/* taskbarAlpha = */ 0,
+                ALPHA_CHANNEL_TASKBAR_STASH, /* updateTaskbarAlpha = */ false);
+    }
+
     /**
      * Resets and updates the icon alignment.
      */
@@ -728,7 +817,7 @@
     }
 
     private void onIconAlignmentRatioChanged() {
-        float currentValue = mIconAlphaForHome.getValue();
+        float currentValue = mTaskbarAlphaForHome.getValue();
         boolean taskbarWillBeVisible = mIconAlignment.value < 1;
         boolean firstFrameVisChanged = (taskbarWillBeVisible && Float.compare(currentValue, 1) != 0)
                 || (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0);
@@ -736,24 +825,33 @@
         mControllers.taskbarViewController.setLauncherIconAlignment(
                 mIconAlignment.value, mLauncher.getDeviceProfile());
         mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value);
-        // Switch taskbar and hotseat in last frame
-        updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0);
+        // Switch taskbar and hotseat in last frame and if taskbar is not hidden for bubbles
+        boolean isHiddenForBubbles = mControllers.taskbarStashController.isHiddenForBubbles();
+        updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0, ALPHA_CHANNEL_TASKBAR_ALIGNMENT,
+                /* updateTaskbarAlpha = */ !isHiddenForBubbles);
 
         // Sync the first frame where we swap taskbar and hotseat.
         if (firstFrameVisChanged && mCanSyncViews && !Utilities.isRunningInTestHarness()) {
             ViewRootSync.synchronizeNextDraw(mLauncher.getHotseat(),
                     mControllers.taskbarActivityContext.getDragLayer(),
-                    () -> {
-                    });
+                    () -> {});
         }
     }
 
-    private void updateIconAlphaForHome(float alpha) {
-        if (mControllers.taskbarActivityContext.isDestroyed()) {
+    private void updateIconAlphaForHome(float taskbarAlpha, @HotseatQsbAlphaId int alphaChannel) {
+        updateIconAlphaForHome(taskbarAlpha, alphaChannel, /* updateTaskbarAlpha = */ true);
+    }
+
+    private void updateIconAlphaForHome(float taskbarAlpha,
+            @HotseatQsbAlphaId int alphaChannel,
+            boolean updateTaskbarAlpha) {
+        if (mIsDestroyed) {
             return;
         }
-        mIconAlphaForHome.setValue(alpha);
-        boolean hotseatVisible = alpha == 0
+        if (updateTaskbarAlpha) {
+            mTaskbarAlphaForHome.setValue(taskbarAlpha);
+        }
+        boolean hotseatVisible = taskbarAlpha == 0
                 || mControllers.taskbarActivityContext.isPhoneMode()
                 || (!mControllers.uiController.isHotseatIconOnTopWhenAligned()
                 && mIconAlignment.value > 0);
@@ -761,12 +859,71 @@
          * Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets
          * should not be visible at the same time.
          */
-        mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0);
+        float targetAlpha = hotseatVisible ? 1 : 0;
+        mLauncher.getHotseat().setIconsAlpha(targetAlpha, alphaChannel);
         if (mIsQsbInline) {
-            mLauncher.getHotseat().setQsbAlpha(hotseatVisible ? 1 : 0);
+            mLauncher.getHotseat().setQsbAlpha(targetAlpha, alphaChannel);
         }
     }
 
+    /** 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;
@@ -851,8 +1008,9 @@
         pw.println(String.format(
                 "%s\tmTaskbarBackgroundAlpha=%.2f", prefix, mTaskbarBackgroundAlpha.value));
         pw.println(String.format(
-                "%s\tmIconAlphaForHome=%.2f", prefix, mIconAlphaForHome.getValue()));
-        pw.println(String.format("%s\tmPrevState=%s", prefix, getStateString(mPrevState)));
+                "%s\tmTaskbarAlphaForHome=%.2f", prefix, mTaskbarAlphaForHome.getValue()));
+        pw.println(String.format("%s\tmPrevState=%s", prefix,
+                mPrevState == null ? null : getStateString(mPrevState)));
         pw.println(String.format("%s\tmState=%s", prefix, getStateString(mState)));
         pw.println(String.format("%s\tmLauncherState=%s", prefix, mLauncherState));
         pw.println(String.format(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index f411e79..0807ee9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.taskbar;
 
 import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
@@ -60,6 +59,8 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
 import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider;
@@ -70,7 +71,9 @@
 import com.android.quickstep.AllAppsActionManager;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -114,7 +117,6 @@
     private WindowManager mWindowManager;
     private FrameLayout mTaskbarRootLayout;
     private boolean mAddedWindow;
-    private boolean mIsSuspended;
     private final TaskbarNavButtonController mNavButtonController;
     private final ComponentCallbacks mComponentCallbacks;
 
@@ -130,6 +132,8 @@
 
     private TaskbarActivityContext mTaskbarActivityContext;
     private StatefulActivity mActivity;
+    private RecentsViewContainer mRecentsViewContainer;
+
     /**
      * Cache a copy here so we can initialize state whenever taskbar is recreated, since
      * this class does not get re-initialized w/ new taskbars.
@@ -171,6 +175,9 @@
                                 + "onActivityDestroyed.");
                 mActivity.removeEventCallback(EVENT_DESTROYED, this);
             }
+            if (mActivity == mRecentsViewContainer) {
+                mRecentsViewContainer = null;
+            }
             mActivity = null;
             debugWhyTaskbarNotDestroyed("clearActivity");
             if (mTaskbarActivityContext != null) {
@@ -209,14 +216,16 @@
                 }
             };
 
+    @NonNull private final DesktopVisibilityController mDesktopVisibilityController;
+
     @SuppressLint("WrongConstant")
     public TaskbarManager(
             Context context,
             AllAppsActionManager allAppsActionManager,
-            TaskbarNavButtonCallbacks navCallbacks) {
-
+            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);
@@ -224,6 +233,7 @@
         mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 ? context.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
                 : null;
+        mDesktopVisibilityController = desktopVisibilityController;
         if (enableTaskbarNoRecreate()) {
             mWindowManager = mContext.getSystemService(WindowManager.class);
             mTaskbarRootLayout = new FrameLayout(mContext) {
@@ -243,8 +253,9 @@
                 context,
                 navCallbacks,
                 SystemUiProxy.INSTANCE.get(mContext),
+                ContextualEduStatsManager.INSTANCE.get(mContext),
                 new Handler(),
-                AssistUtils.newInstance(mContext));
+                ContextualSearchInvoker.newInstance(mContext));
         mComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
 
@@ -399,9 +410,28 @@
         }
         mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
 
+        if (activity instanceof RecentsViewContainer recentsViewContainer) {
+            setRecentsViewContainer(recentsViewContainer);
+        }
+    }
+
+    /**
+     * Sets the current RecentsViewContainer, from which we create a TaskbarUIController.
+     */
+    public void setRecentsViewContainer(@NonNull RecentsViewContainer recentsViewContainer) {
+        if (mRecentsViewContainer == recentsViewContainer) {
+            return;
+        }
+        if (mRecentsViewContainer == mActivity) {
+            // When switching to RecentsWindowManager (not an Activity), the old mActivity is not
+            // destroyed, nor is there a new Activity to replace it. Thus if we don't clear it here,
+            // it will not get re-set properly if we return to the Activity (e.g. NexusLauncher).
+            mActivityOnDestroyCallback.run();
+        }
+        mRecentsViewContainer = recentsViewContainer;
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+                    createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
         }
     }
 
@@ -424,12 +454,18 @@
     /**
      * Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
      */
-    private TaskbarUIController createTaskbarUIControllerForActivity(StatefulActivity activity) {
-        if (activity instanceof QuickstepLauncher) {
-            return new LauncherTaskbarUIController((QuickstepLauncher) activity);
+    private TaskbarUIController createTaskbarUIControllerForRecentsViewContainer(
+            RecentsViewContainer container) {
+        if (container instanceof QuickstepLauncher quickstepLauncher) {
+            return new LauncherTaskbarUIController(quickstepLauncher);
         }
-        if (activity instanceof RecentsActivity) {
-            return new FallbackTaskbarUIController((RecentsActivity) activity);
+        // If a 3P Launcher is default, always use FallbackTaskbarUIController regardless of
+        // whether the recents container is RecentsActivity or RecentsWindowManager.
+        if (container instanceof RecentsActivity recentsActivity) {
+            return new FallbackTaskbarUIController<>(recentsActivity);
+        }
+        if (container instanceof RecentsWindowManager recentsWindowManager) {
+            return new FallbackTaskbarUIController<>(recentsWindowManager);
         }
         return TaskbarUIController.DEFAULT;
     }
@@ -441,8 +477,6 @@
      */
     @VisibleForTesting
     public synchronized void recreateTaskbar() {
-        if (mIsSuspended) return;
-
         Trace.beginSection("recreateTaskbar");
         try {
             DeviceProfile dp = mUserUnlocked ?
@@ -459,16 +493,18 @@
                 + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
                 + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
-            if (!isTaskbarEnabled) {
+            if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
                 SystemUiProxy.INSTANCE.get(mContext)
                     .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
-                return;
+                if (!isTaskbarEnabled) {
+                    return;
+                }
             }
 
             if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) {
                 mTaskbarActivityContext = new TaskbarActivityContext(mContext,
                         mNavigationBarPanelContext, dp, mNavButtonController,
-                        mUnfoldProgressProvider);
+                        mUnfoldProgressProvider, mDesktopVisibilityController);
             } else {
                 mTaskbarActivityContext.updateDeviceProfile(dp);
             }
@@ -477,9 +513,9 @@
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
             mTaskbarActivityContext.init(mSharedState);
 
-            if (mActivity != null) {
+            if (mRecentsViewContainer != null) {
                 mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+                        createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
             }
 
             if (enableTaskbarNoRecreate()) {
@@ -527,27 +563,35 @@
         }
     }
 
-    public void checkNavBarModes() {
+    public void checkNavBarModes(int displayId) {
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.checkNavBarModes();
         }
     }
 
-    public void finishBarAnimations() {
+    public void finishBarAnimations(int displayId) {
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.finishBarAnimations();
         }
     }
 
-    public void touchAutoDim(boolean reset) {
+    public void touchAutoDim(int displayId, boolean reset) {
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.touchAutoDim(reset);
         }
     }
 
-    public void transitionTo(@BarTransitions.TransitionMode int barMode,
+    public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode,
             boolean animate) {
-        mTaskbarActivityContext.transitionTo(barMode, animate);
+        if (mTaskbarActivityContext != null) {
+            mTaskbarActivityContext.transitionTo(barMode, animate);
+        }
+    }
+
+    public void appTransitionPending(boolean pending) {
+        if (mTaskbarActivityContext != null) {
+            mTaskbarActivityContext.appTransitionPending(pending);
+        }
     }
 
     private boolean isTaskbarEnabled(DeviceProfile deviceProfile) {
@@ -618,6 +662,7 @@
      * Called when the manager is no longer needed
      */
     public void destroy() {
+        mRecentsViewContainer = null;
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
         mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
@@ -648,21 +693,6 @@
         }
     }
 
-    /**
-     * Removes Taskbar from the window manager and prevents recreation if {@code true}.
-     * <p>
-     * Suspending is for testing purposes only; avoid calling this method in production.
-     */
-    @VisibleForTesting
-    public void setSuspended(boolean isSuspended) {
-        mIsSuspended = isSuspended;
-        if (mIsSuspended) {
-            removeTaskbarRootViewFromWindow();
-        } else {
-            addTaskbarRootViewToWindow();
-        }
-    }
-
     private void addTaskbarRootViewToWindow() {
         if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
             mWindowManager.addView(mTaskbarRootLayout,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 5024cd8..c20617d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -42,7 +42,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -196,26 +195,21 @@
         final TaskbarRecentAppsController recentAppsController =
                 mControllers.taskbarRecentAppsController;
         hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
-        Set<Integer> runningTaskIds = recentAppsController.getRunningTaskIds();
-        Set<Integer> minimizedTaskIds = recentAppsController.getMinimizedTaskIds();
 
         if (mDeferUpdatesForSUW) {
             ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
             mDeferredUpdates = () ->
                     commitHotseatItemUpdates(finalHotseatItemInfos,
-                            recentAppsController.getShownTasks(), runningTaskIds,
-                            minimizedTaskIds);
+                            recentAppsController.getShownTasks());
         } else {
-            commitHotseatItemUpdates(hotseatItemInfos,
-                    recentAppsController.getShownTasks(), runningTaskIds, minimizedTaskIds);
+            commitHotseatItemUpdates(hotseatItemInfos, recentAppsController.getShownTasks());
         }
     }
 
-    private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
-            Set<Integer> runningTaskIds, Set<Integer> minimizedTaskIds) {
+    private void commitHotseatItemUpdates(
+            ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
         mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
-        mControllers.taskbarViewController.updateIconViewsRunningStates(
-                runningTaskIds, minimizedTaskIds);
+        mControllers.taskbarViewController.updateIconViewsRunningStates();
     }
 
     /**
@@ -250,6 +244,7 @@
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         Preconditions.assertUIThread();
         mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap);
+        mControllers.taskbarPopupController.setApps(apps);
     }
 
     protected void dumpLogs(String prefix, PrintWriter pw) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 872a4d0..0f9ede9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.taskbar;
 
+import static android.view.MotionEvent.ACTION_UP;
+
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS;
@@ -31,12 +33,14 @@
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
 
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.Flags;
 
@@ -45,12 +49,14 @@
 import androidx.annotation.StringRes;
 
 import com.android.launcher3.R;
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.testing.TestLogging;
 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;
 
 import java.io.PrintWriter;
@@ -109,8 +115,9 @@
     private final Context mContext;
     private final TaskbarNavButtonCallbacks mCallbacks;
     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;
@@ -119,13 +126,15 @@
             Context context,
             TaskbarNavButtonCallbacks callbacks,
             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) {
@@ -136,15 +145,18 @@
         view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
         switch (buttonType) {
             case BUTTON_BACK:
-                logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
-                executeBack();
+                executeBack(/* keyEvent */ null);
                 break;
             case BUTTON_HOME:
                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP);
+                mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
+                        GestureType.HOME);
                 navigateHome();
                 break;
             case BUTTON_RECENTS:
                 logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP);
+                mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
+                        GestureType.OVERVIEW);
                 navigateToOverview();
                 break;
             case BUTTON_IME_SWITCH:
@@ -171,7 +183,9 @@
 
         // Provide the same haptic feedback that the system offers for long press.
         // The haptic feedback from long pressing on the home button is handled by circle to search.
-        if (buttonType != BUTTON_HOME) {
+        // There are no haptics for long pressing the back button if predictive back is enabled
+        if (buttonType != BUTTON_HOME
+                && (!predictiveBackThreeButtonNav() || buttonType != BUTTON_BACK)) {
             view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
         }
         switch (buttonType) {
@@ -309,8 +323,13 @@
         mCallbacks.onToggleOverview();
     }
 
-    private void executeBack() {
-        mSystemUiProxy.onBackPressed();
+    void executeBack(@Nullable KeyEvent keyEvent) {
+        if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) {
+            logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
+            mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
+                    GestureType.BACK);
+        }
+        mSystemUiProxy.onBackEvent(keyEvent);
     }
 
     private void onImeSwitcherPress() {
@@ -333,8 +352,9 @@
         if (mScreenPinned || !mAssistantLongPressEnabled) {
             return;
         }
-        // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation.
-        if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
+        // Attempt to start Contextual Search, otherwise fall back to SysUi's implementation.
+        if (!mContextualSearchInvoker.tryStartAssistOverride(
+                INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
             Bundle args = new Bundle();
             args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
             mSystemUiProxy.startAssistant(args);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
new file mode 100644
index 0000000..126e9bb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -0,0 +1,260 @@
+/*
+ * 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.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Reorderable;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiTranslateDelegate;
+import com.android.launcher3.util.Themes;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * View used as overflow icon within task bar, when the list of recent/running apps overflows the
+ * available display bounds - if display is not wide enough to show all running apps in the taskbar,
+ * this icon is added to the taskbar as an entry point to open UI that surfaces all running apps.
+ * The icon contains icon representations of up to 4 more recent tasks in overflow, stacked on top
+ * each other in counter clockwise manner (icons of tasks partially overlapping with each other).
+ */
+public class TaskbarOverflowView extends FrameLayout implements Reorderable {
+    private static final int MAX_ITEMS_IN_PREVIEW = 4;
+
+    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;
+    private float mItemPreviewStrokeWidth;
+
+    // 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;
+
+    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;
+        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);
+        mItemPreviewStrokeWidth = getResources().getDimension(
+                R.dimen.taskbar_overflow_button_preview_stroke);
+
+        setWillNotDraw(false);
+    }
+
+    @Override
+    protected void onDraw(@NonNull Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mIsActive) {
+            drawLeaveBehindCircle(canvas);
+        } else {
+            drawAppIcons(canvas);
+        }
+    }
+
+    private void drawAppIcons(@NonNull Canvas canvas) {
+        mItemBackgroundPaint.setColor(mItemBackgroundColor);
+        float radius = mIconSize / 2f - mPadding;
+
+        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;
+            }
+
+            // Set the item icon size so two items fit within the overflow icon with stroke width
+            // included, and overlap of 4 stroke width sizes between base item preview items.
+            // 2 * strokeWidth + 2 * itemIconSize - 4 * strokeWidth = iconSize = 2 * radius.
+            float itemIconSize = radius + mItemPreviewStrokeWidth;
+            // Offset item icon from center so item icon stroke edge matches the parent icon edge.
+            float itemCenterOffset = radius - itemIconSize / 2 - mItemPreviewStrokeWidth;
+
+            float itemCenterX = getItemXOffset(itemCenterOffset, mIsRtlLayout, i, itemsToShow);
+            float itemCenterY = getItemYOffset(itemCenterOffset, i, itemsToShow);
+
+            Drawable iconCopy = icon.getConstantState().newDrawable().mutate();
+            iconCopy.setBounds(0, 0, (int) itemIconSize, (int) itemIconSize);
+
+            canvas.save();
+            float itemIconRadius = itemIconSize / 2;
+            canvas.translate(
+                    mPadding + itemCenterX + radius - itemIconRadius,
+                    mPadding + itemCenterY + radius - itemIconRadius);
+            canvas.drawCircle(itemIconRadius, itemIconRadius,
+                    itemIconRadius + mItemPreviewStrokeWidth, mItemBackgroundPaint);
+            iconCopy.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+    private void drawLeaveBehindCircle(@NonNull Canvas canvas) {
+        mItemBackgroundPaint.setColor(mLeaveBehindColor);
+
+        final var xyCenter = mIconSize / 2f;
+        canvas.drawCircle(xyCenter, xyCenter, mIconSize / 4f, 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) {
+            mIsActive = isActive;
+            invalidate();
+        }
+    }
+
+    @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 6c9cc64..bcfc718 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -30,12 +30,11 @@
 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,
-    private val isInDesktopModeProvider: () -> Boolean,
-) : TaskbarControllers.LoggableTaskbarController {
+class TaskbarPinningController(private val context: TaskbarActivityContext) :
+    TaskbarControllers.LoggableTaskbarController {
 
     private lateinit var controllers: TaskbarControllers
     private lateinit var taskbarSharedState: TaskbarSharedState
@@ -58,7 +57,7 @@
                     return
                 }
                 val shouldPinTaskbar =
-                    if (isInDesktopModeProvider()) {
+                    if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) {
                         !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
                     } else {
                         !launcherPrefs.get(TASKBAR_PINNING)
@@ -78,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)
@@ -91,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,9 +118,13 @@
             dragLayerController.taskbarBackgroundProgress.animateToValue(animateToValue),
             taskbarViewController.taskbarIconTranslationYForPinning.animateToValue(animateToValue),
             taskbarViewController.taskbarIconScaleForPinning.animateToValue(animateToValue),
-            taskbarViewController.taskbarIconTranslationXForPinning.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
     }
@@ -134,10 +137,10 @@
     @VisibleForTesting
     fun recreateTaskbarAndUpdatePinningValue() {
         updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false)
-        if (isInDesktopModeProvider()) {
+        if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) {
             launcherPrefs.put(
                 TASKBAR_PINNING_IN_DESKTOP_MODE,
-                !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
+                !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE),
             )
         } else {
             launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING))
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index b697590..70d4bb1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
 
 import android.content.Intent;
@@ -29,11 +30,13 @@
 import com.android.internal.logging.InstanceId;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -51,8 +54,11 @@
 import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.LogUtils;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
@@ -69,12 +75,16 @@
     private static final SystemShortcut.Factory<BaseTaskbarContext>
             APP_INFO = SystemShortcut.AppInfo::new;
 
+    private static final SystemShortcut.Factory<BaseTaskbarContext>
+            BUBBLE = SystemShortcut.BubbleShortcut::new;
+
     private final TaskbarActivityContext mContext;
     private final PopupDataProvider mPopupDataProvider;
 
     // Initialized in init.
     private TaskbarControllers mControllers;
     private boolean mAllowInitialSplitSelection;
+    private AppInfo[] mAppInfosList;
 
     public TaskbarPopupController(TaskbarActivityContext context) {
         mContext = context;
@@ -181,11 +191,21 @@
 
     // Create a Stream of all applicable system shortcuts
     private Stream<SystemShortcut.Factory> getSystemShortcuts() {
-        // append split options to APP_INFO shortcut, the order here will reflect in the popup
-        return Stream.concat(
-                Stream.of(APP_INFO),
-                mControllers.uiController.getSplitMenuOptions()
-        );
+        // append split options to APP_INFO shortcut if not in Desktop Windowing mode, the order
+        // here will reflect in the popup
+        ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>();
+        shortcuts.add(APP_INFO);
+        if (!mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) {
+            shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList());
+        }
+        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+            shortcuts.add(BUBBLE);
+        }
+        if (Flags.enableMultiInstanceMenuTaskbar()
+                && DesktopModeStatus.canEnterDesktopMode(mContext)) {
+            shortcuts.addAll(getMultiInstanceMenuOptions().toList());
+        }
+        return shortcuts.stream();
     }
 
     @Override
@@ -248,7 +268,55 @@
                 originalView, position, mAllowInitialSplitSelection);
     }
 
-     /**
+    /**
+     * Set the list of AppInfos to be able to pull from later
+     */
+    public void setApps(AppInfo[] apps) {
+        mAppInfosList = apps;
+    }
+
+    /**
+     * Finds and returns an AppInfo object from a list, using its ComponentKey for identification.
+     * Based off of {@link com.android.launcher3.allapps.AllAppsStore#getApp(ComponentKey)}
+     * since we cannot access AllAppsStore from here.
+     */
+    public AppInfo getApp(ComponentKey key) {
+        if (key == null) {
+            return null;
+        }
+        AppInfo tempInfo = new AppInfo();
+        tempInfo.componentName = key.componentName;
+        tempInfo.user = key.user;
+        int index = Arrays.binarySearch(mAppInfosList, tempInfo, COMPONENT_KEY_COMPARATOR);
+        return index < 0 ? null : mAppInfosList[index];
+    }
+
+    /**
+     * 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();
+
+    }
+
+    /**
+     * Creates a factory function representing a "New Window" menu item only if the calling app
+     * supports multi-instance.
+     * @return A factory function to be used in populating the long-press menu.
+     */
+    SystemShortcut.Factory<BaseTaskbarContext> createNewWindowShortcutFactory() {
+        return (context, itemInfo, originalView) -> {
+            ComponentKey key = itemInfo.getComponentKey();
+            AppInfo app = getApp(key);
+            if (app != null && app.supportsMultiInstance()) {
+                return new NewWindowTaskbarShortcut<>(context, itemInfo, originalView);
+            }
+            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 5c08116..3d57de4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,19 +15,20 @@
  */
 package com.android.launcher3.taskbar
 
+import android.content.Context
+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
 import com.android.launcher3.model.data.WorkspaceItemInfo
-import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
 import com.android.launcher3.util.CancellableTask
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
-import com.android.window.flags.Flags.enableDesktopWindowingMode
-import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import java.io.PrintWriter
 
 /**
@@ -35,19 +36,18 @@
  * - When in Fullscreen mode: show the N most recent Tasks
  * - When in Desktop Mode: show the currently running (open) Tasks
  */
-class TaskbarRecentAppsController(
-    private val recentsModel: RecentsModel,
-    // Pass a provider here instead of the actual DesktopVisibilityController instance since that
-    // instance might not be available when this constructor is called.
-    private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?,
-) : LoggableTaskbarController {
+class TaskbarRecentAppsController(context: Context, private val recentsModel: RecentsModel) :
+    LoggableTaskbarController {
 
-    // TODO(b/335401172): unify DesktopMode checks in Launcher.
     var canShowRunningApps =
-        enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
+        DesktopModeStatus.canEnterDesktopMode(context) &&
+            DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue
         @VisibleForTesting
         set(isEnabledFromTest) {
             field = isEnabledFromTest
+            if (!field && !canShowRecentApps) {
+                recentsModel.unregisterRecentTasksChangedListener()
+            }
         }
 
     // TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
@@ -55,6 +55,9 @@
         @VisibleForTesting
         set(isEnabledFromTest) {
             field = isEnabledFromTest
+            if (!field && !canShowRunningApps) {
+                recentsModel.unregisterRecentTasksChangedListener()
+            }
         }
 
     // Initialized in init.
@@ -70,31 +73,69 @@
     var shownTasks: List<GroupTask> = emptyList()
         private set
 
-    private val desktopVisibilityController: DesktopVisibilityController?
-        get() = desktopVisibilityControllerProvider()
+    /**
+     * 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 val isInDesktopMode: Boolean
-        get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
+    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.
          * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet().
          */
         get() {
-            if (!canShowRunningApps || !isInDesktopMode) {
+            if (
+                !canShowRunningApps ||
+                    !controllers.taskbarDesktopModeController.areDesktopTasksVisible
+            ) {
                 return emptySet()
             }
             val tasks = desktopTask?.tasks ?: return emptySet()
             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.
          */
         get() {
-            if (!canShowRunningApps || !isInDesktopMode) {
+            if (
+                !canShowRunningApps ||
+                    !controllers.taskbarDesktopModeController.areDesktopTasksVisible
+            ) {
                 return emptySet()
             }
             val desktopTasks = desktopTask?.tasks ?: return emptySet()
@@ -114,8 +155,10 @@
 
     fun init(taskbarControllers: TaskbarControllers) {
         controllers = taskbarControllers
-        recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
-        reloadRecentTasksIfNeeded()
+        if (canShowRunningApps || canShowRecentApps) {
+            recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
+            controllers.runAfterInit { reloadRecentTasksIfNeeded() }
+        }
     }
 
     fun onDestroy() {
@@ -127,8 +170,10 @@
     /** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */
     fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo?>): Array<ItemInfo?> {
         // Ignore predicted apps - we show running or recent apps instead.
+        val areDesktopTasksVisible = controllers.taskbarDesktopModeController.areDesktopTasksVisible
         val removePredictions =
-            (isInDesktopMode && canShowRunningApps) || (!isInDesktopMode && canShowRecentApps)
+            (areDesktopTasksVisible && canShowRunningApps) ||
+                (!areDesktopTasksVisible && canShowRecentApps)
         if (!removePredictions) {
             shownHotseatItems = hotseatItems.filterNotNull()
             onRecentsOrHotseatChanged()
@@ -140,11 +185,11 @@
                 .filter { itemInfo -> !itemInfo.isPredictedItem }
                 .toMutableList()
 
-        if (isInDesktopMode && canShowRunningApps) {
+        if (areDesktopTasksVisible && canShowRunningApps) {
             shownHotseatItems =
                 updateHotseatItemsFromRunningTasks(
                     getOrderedAndWrappedDesktopTasks(),
-                    shownHotseatItems
+                    shownHotseatItems,
                 )
         }
 
@@ -189,7 +234,7 @@
         val oldShownTasks = shownTasks
         orderedRunningTaskIds = updateOrderedRunningTaskIds()
         shownTasks =
-            if (isInDesktopMode) {
+            if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) {
                 computeShownRunningTasks()
             } else {
                 computeShownRecentTasks()
@@ -245,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.
@@ -271,7 +316,7 @@
 
     private fun dedupeHotseatTasks(
         groupTasks: List<GroupTask>,
-        shownHotseatItems: List<ItemInfo>
+        shownHotseatItems: List<ItemInfo>,
     ): List<GroupTask> {
         val hotseatPackages = shownHotseatItems.map { item -> item.targetPackage }
         return groupTasks.filter { groupTask ->
@@ -286,7 +331,7 @@
      */
     private fun updateHotseatItemsFromRunningTasks(
         groupTasks: List<GroupTask>,
-        shownHotseatItems: List<ItemInfo>
+        shownHotseatItems: List<ItemInfo>,
     ): List<ItemInfo> =
         shownHotseatItems.map { itemInfo ->
             if (itemInfo is TaskItemInfo) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 48d2bc2..4a7e4f0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -20,14 +20,17 @@
 import static com.android.launcher3.taskbar.bubbles.BubbleBarController.isBubbleBarEnabled;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
-import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
+import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
 
 import android.animation.ObjectAnimator;
 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;
 import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -65,6 +68,7 @@
      */
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
+        onTaskbarVisibilityChanged(mControllers.taskbarViewController.getTaskbarVisibility());
     }
 
     /**
@@ -75,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 */);
         }
@@ -85,24 +89,39 @@
      * Updates the scrim state based on the flags.
      */
     public void updateStateForSysuiFlags(@SystemUiStateFlags long stateFlags, boolean skipAnim) {
+        if (mActivity.isPhoneMode()) {
+            // There is no scrim for the bar in the phone mode.
+            return;
+        }
         if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) {
             // These scrims aren't used if bubble bar & transient taskbar are active.
             return;
         }
         mSysUiStateFlags = stateFlags;
-        showScrim(shouldShowScrim(), getScrimAlpha(), skipAnim);
+        showScrim(shouldShowScrim(), computeScrimAlpha(), skipAnim);
     }
 
     private boolean shouldShowScrim() {
         final boolean bubblesExpanded = (mSysUiStateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0;
         boolean isShadeVisible = (mSysUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0;
+        BubbleControllers bubbleControllers = mActivity.getBubbleControllers();
+        boolean isBubbleControllersPresented = bubbleControllers != null;
+        // when the taskbar is in persistent mode, we hide the task bar icons on bubble bar expand,
+        // which makes the taskbar invisible, so need to check if the bubble bar is not on home
+        // to show the scrim view
+        boolean showScrimForBubbles = bubblesExpanded
+                && !mTaskbarVisible
+                && isBubbleControllersPresented
+                && !DisplayController.isTransientTaskbar(mActivity)
+                && !bubbleControllers.bubbleStashController.isBubblesShowingOnHome();
         return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible()
                 && !isShadeVisible
                 && !mControllers.taskbarStashController.isStashed()
-                && mTaskbarVisible;
+                && (mTaskbarVisible || showScrimForBubbles)
+                && !mControllers.taskbarStashController.isHiddenForBubbles();
     }
 
-    private float getScrimAlpha() {
+    private float computeScrimAlpha() {
         final boolean isPersistentTaskBarVisible =
                 mTaskbarVisible && !DisplayController.isTransientTaskbar(mScrimView.getContext());
         final boolean manageMenuExpanded =
@@ -123,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);
@@ -136,7 +155,7 @@
     }
 
     private void onClick() {
-        SystemUiProxy.INSTANCE.get(mActivity).onBackPressed();
+        SystemUiProxy.INSTANCE.get(mActivity).onBackEvent(null);
     }
 
     @Override
@@ -150,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 77bd35f..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];
 
@@ -102,5 +115,8 @@
 
     // To track if taskbar was stashed / unstashed between configuration changes (which recreates
     // the task bar).
-    public Boolean taskbarWasStashedAuto = true;
+    public boolean taskbarWasStashedAuto = true;
+
+    // should show corner radius on persistent taskbar when in desktop mode.
+    public boolean showCornerRadiusInDesktopMode = false;
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 267e19c..c1dd216 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
+import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -61,11 +62,10 @@
 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.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
-import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.SystemUiFlagUtils;
 
@@ -83,6 +83,13 @@
     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)} */
+    public static boolean SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE = true;
+
     public static final int FLAG_IN_APP = 1 << 0;
     public static final int FLAG_STASHED_IN_APP_SYSUI = 1 << 1; // shade open, ...
     public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 2; // setup wizard and AllSetActivity
@@ -95,6 +102,10 @@
     public static final int FLAG_STASHED_SYSUI = 1 << 9; //  app pinning,...
     public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ...
     public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview
+    // 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;
@@ -116,26 +127,30 @@
 
     // If any of these flags are enabled, the taskbar must be stashed.
     private static final int FLAGS_FORCE_STASHED = FLAG_STASHED_SYSUI | FLAG_STASHED_DEVICE_LOCKED
-            | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN;
+            | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN
+            | FLAG_STASHED_FOR_BUBBLES;
 
     /**
      * How long to stash/unstash when manually invoked via long press.
      *
      * 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.
@@ -151,12 +166,12 @@
     /**
      * How long to delay the icon/stash handle alpha.
      */
-    private static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
+    public static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
 
     /**
      * How long the icon/stash handle alpha animation plays.
      */
-    private 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.
@@ -178,7 +193,7 @@
 
     // Duration for which an unlock event is considered "current", as other events are received
     // asynchronously.
-    private static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
+    public static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
 
     /**
      * The default stash animation, morphing the taskbar into the navbar.
@@ -202,6 +217,13 @@
      */
     private static final int TRANSITION_UNSTASH_SUW_MANUAL = 3;
 
+    /**
+     * total duration of entering dream state animation, which we use as start delay to
+     * applyState() when SYSUI_STATE_DEVICE_DREAMING flag is present. Keep this in sync with
+     * DreamAnimationController.TOTAL_ANIM_DURATION.
+     */
+    private static final int SKIP_TOTAL_DREAM_ANIM_DURATION = 450;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
             TRANSITION_DEFAULT,
@@ -245,7 +267,7 @@
     private boolean mEnableBlockingTimeoutDuringTests = false;
 
     private Animator mTaskbarBackgroundAlphaAnimator;
-    private long mTaskbarBackgroundDuration;
+    private final long mTaskbarBackgroundDuration;
     private boolean mUserIsNotGoingHome = false;
 
     // Evaluate whether the handle should be stashed
@@ -331,9 +353,10 @@
         applyState(/* duration = */ 0);
 
         // Hide the background while stashed so it doesn't show on fast swipes home
-        boolean shouldHideTaskbarBackground = enableScalingRevealHomeAnimation()
-                && DisplayController.isTransientTaskbar(mActivity)
-                && isStashed();
+        boolean shouldHideTaskbarBackground = mActivity.isPhoneMode() ||
+                (enableScalingRevealHomeAnimation()
+                        && DisplayController.isTransientTaskbar(mActivity)
+                        && isStashed());
 
         mTaskbarBackgroundAlphaForStash.setValue(shouldHideTaskbarBackground ? 0 : 1);
 
@@ -388,6 +411,24 @@
     }
 
     /**
+     * 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.
+     * * 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();
+    }
+
+    /**
      * Returns whether the taskbar should be stashed in apps (e.g. user long pressed to stash).
      */
     public boolean isStashedInApp() {
@@ -421,6 +462,21 @@
         return hasAnyFlag(FLAGS_IN_APP);
     }
 
+    /** Returns whether the taskbar is currently in overview screen. */
+    public boolean isInOverview() {
+        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);
+    }
+
     /**
      * Returns the height that taskbar will be touchable.
      */
@@ -483,9 +539,17 @@
     /**
      * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION.
      * If bubble bar exists, it will match taskbars stashing behavior.
+     * Will not delay taskbar background by default.
      */
     public void updateAndAnimateTransientTaskbar(boolean stash) {
-        updateAndAnimateTransientTaskbar(stash, /* shouldBubblesFollow= */ true);
+        updateAndAnimateTransientTaskbar(stash, SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE, false);
+    }
+
+    /**
+     * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION.
+     */
+    public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) {
+        updateAndAnimateTransientTaskbar(stash, shouldBubblesFollow, false);
     }
 
     /**
@@ -493,28 +557,47 @@
      *
      * @param stash               whether transient taskbar should be stashed.
      * @param shouldBubblesFollow whether bubbles should match taskbars behavior.
+     * @param delayTaskbarBackground whether we will delay the taskbar background animation
      */
-    public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) {
+    public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow,
+            boolean delayTaskbarBackground) {
         if (!DisplayController.isTransientTaskbar(mActivity)) {
             return;
         }
 
-        if (
-                stash
-                        && !mControllers.taskbarAutohideSuspendController
-                        .isSuspendedForTransientTaskbarInLauncher()
-                        && mControllers.taskbarAutohideSuspendController
-                        .isTransientTaskbarStashingSuspended()) {
+        if (stash
+                && !mControllers.taskbarAutohideSuspendController
+                .isSuspendedForTransientTaskbarInLauncher()
+                && mControllers.taskbarAutohideSuspendController
+                .isTransientTaskbarStashingSuspended()) {
             // Avoid stashing if autohide is currently suspended.
             return;
         }
 
+        boolean shouldApplyState = false;
+
+        if (delayTaskbarBackground) {
+            mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG, true);
+            shouldApplyState = true;
+        }
+
         if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) {
             mTaskbarSharedState.taskbarWasStashedAuto = stash;
             updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
+            shouldApplyState = true;
+        }
+
+        if (shouldApplyState) {
             applyState();
         }
 
+        // Effectively a no-opp to remove the tag.
+        if (delayTaskbarBackground) {
+            mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG,
+                    false);
+            mControllers.taskbarStashController.applyState(0);
+        }
+
         mControllers.bubbleControllers.ifPresent(controllers -> {
             if (shouldBubblesFollow) {
                 final boolean willStash = mIsStashedPredicate.test(mState);
@@ -568,6 +651,7 @@
                 /* isStashed= */ mActivity.isPhoneMode(),
                 placeholderDuration,
                 TRANSITION_UNSTASH_SUW_MANUAL,
+                /* skipTaskbarBackgroundDelay */ false,
                 /* jankTag= */ "SUW_MANUAL");
         animation.addListener(AnimatorListeners.forEndCallback(
                 () -> mControllers.taskbarViewController.setDeferUpdatesForSUW(false)));
@@ -577,13 +661,14 @@
     /**
      * Create a stash animation and save to {@link #mAnimator}.
      *
-     * @param isStashed     whether it's a stash animation or an unstash animation
-     * @param duration      duration of the animation
-     * @param animationType what transition type to play.
-     * @param jankTag       tag to be used in jank monitor trace.
+     * @param isStashed             whether it's a stash animation or an unstash animation
+     * @param duration              duration of the animation
+     * @param animationType         what transition type to play.
+     * @param shouldDelayBackground whether we should delay the taskbar bg animation
+     * @param jankTag               tag to be used in jank monitor trace.
      */
     private void createAnimToIsStashed(boolean isStashed, long duration,
-            @StashAnimation int animationType, String jankTag) {
+            @StashAnimation int animationType, boolean shouldDelayBackground, String jankTag) {
         if (animationType == TRANSITION_UNSTASH_SUW_MANUAL && isStashed) {
             // The STASH_ANIMATION_SUW_MANUAL must only be used during an unstash animation.
             Log.e(TAG, "Illegal arguments:Using TRANSITION_UNSTASH_SUW_MANUAL to stash taskbar");
@@ -621,7 +706,8 @@
         }
 
         if (isTransientTaskbar) {
-            createTransientAnimToIsStashed(mAnimator, isStashed, duration, animationType);
+            createTransientAnimToIsStashed(mAnimator, isStashed, duration,
+                    shouldDelayBackground, animationType);
         } else {
             createAnimToIsStashed(mAnimator, isStashed, duration, stashTranslation, animationType);
         }
@@ -727,7 +813,7 @@
     }
 
     private void createTransientAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration,
-            @StashAnimation int animationType) {
+            boolean shouldDelayBackground, @StashAnimation int animationType) {
         // Target values of the properties this is going to set
         final float backgroundOffsetTarget = isStashed ? 1 : 0;
         final float iconAlphaTarget = isStashed ? 0 : 1;
@@ -741,14 +827,14 @@
             if (animationType == TRANSITION_HANDLE_FADE) {
                 // When fading, the handle fades in/out at the beginning of the transition with
                 // TASKBAR_STASH_ALPHA_DURATION.
-                backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
+                backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
                 // The iconAlphaDuration must be set to duration for the skippable interpolators
                 // below to work.
                 iconAlphaDuration = duration;
             } else {
                 iconAlphaStartDelay = TASKBAR_STASH_ALPHA_START_DELAY;
-                iconAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
-                backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
+                iconAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
+                backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
 
                 if (isStashed) {
                     if (animationType == TRANSITION_HOME_TO_APP) {
@@ -765,7 +851,10 @@
                 backgroundAndHandleAlphaStartDelay,
                 backgroundAndHandleAlphaDuration, LINEAR);
 
-        if (enableScalingRevealHomeAnimation() && !isStashed) {
+
+        if (enableScalingRevealHomeAnimation()
+                && !isStashed
+                && shouldDelayBackground) {
             play(as, getTaskbarBackgroundAnimatorWhenNotGoingHome(duration),
                     0, 0, LINEAR);
             as.addListener(AnimatorListeners.forEndCallback(
@@ -890,7 +979,7 @@
         }
         int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND :
                 InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE;
-        animator.addListener(new AnimatorListenerAdapter() {
+        animator.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
                 final Configuration.Builder builder =
@@ -902,9 +991,16 @@
             }
 
             @Override
-            public void onAnimationEnd(@NonNull Animator animation) {
+            public void onAnimationSuccess(@NonNull Animator animator) {
                 InteractionJankMonitor.getInstance().end(action);
             }
+
+            @Override
+            public void onAnimationCancel(@NonNull Animator animation) {
+                super.onAnimationCancel(animation);
+
+                InteractionJankMonitor.getInstance().cancel(action);
+            }
         });
     }
 
@@ -934,13 +1030,29 @@
     }
 
     public void applyState() {
-        applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION);
+        applyState(/* postApplyAction = */ null);
+    }
+
+    /** Applies state and performs action after state is applied. */
+    public void applyState(@Nullable Runnable postApplyAction) {
+        applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION, postApplyAction);
     }
 
     public void applyState(long duration) {
+        applyState(duration, /* postApplyAction = */ null);
+    }
+
+    private void applyState(long duration, @Nullable Runnable postApplyAction) {
         Animator animator = createApplyStateAnimator(duration);
         if (animator != null) {
+            if (postApplyAction != null) {
+                // performs action on animation end
+                animator.addListener(AnimatorListeners.forEndCallback(postApplyAction));
+            }
             animator.start();
+        } else if (postApplyAction != null) {
+            // animator was not created, just execute the action
+            postApplyAction.run();
         }
     }
 
@@ -958,6 +1070,9 @@
      */
     @Nullable
     public Animator createApplyStateAnimator(long duration) {
+        if (mActivity.isPhoneMode()) {
+            return null;
+        }
         return mStatePropertyHolder.createSetStateAnimator(mState, duration);
     }
 
@@ -990,7 +1105,8 @@
     /**
      * When hiding the IME, delay the unstash animation to align with the end of the transition.
      */
-    private long getTaskbarStashStartDelayForIme() {
+    @VisibleForTesting
+    long getTaskbarStashStartDelayForIme() {
         if (mIsImeShowing) {
             // Only delay when IME is exiting, not entering.
             return 0;
@@ -1024,7 +1140,13 @@
             startDelay = getTaskbarStashStartDelayForIme();
         }
 
-        applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay);
+        if (isTaskbarHidden(systemUiStateFlags) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
+            updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
+            applyState(0, SKIP_TOTAL_DREAM_ANIM_DURATION);
+        } else {
+            updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
+            applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay);
+        }
     }
 
     /**
@@ -1046,16 +1168,15 @@
         }
 
         // 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
-        DesktopVisibilityController visibilityController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-        if (visibilityController != null && mActivity.isHardwareKeyboard()
-                && mActivity.isThreeButtonNav() && visibilityController.areDesktopTasksVisible()) {
+        if (isHardwareKeyboard()
+                && mActivity.isThreeButtonNav()
+                && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) {
             return false;
         }
 
@@ -1067,6 +1188,21 @@
         return mIsImeShowing || mIsImeSwitcherShowing;
     }
 
+    private boolean isHardwareKeyboard() {
+        return mActivity.isHardwareKeyboard() && !sEnableSoftwareImeForTests;
+    }
+
+    /**
+     * Overrides {@link #isHardwareKeyboard()} to {@code false} for testing, if enabled.
+     * <p>
+     * Virtual devices are sometimes in hardware keyboard mode, leading to an inconsistent
+     * testing environment.
+     */
+    @VisibleForTesting
+    static void enableSoftwareImeForTests(boolean enable) {
+        sEnableSoftwareImeForTests = enable;
+    }
+
     /**
      * Updates the proper flag to indicate whether the task bar should be stashed.
      *
@@ -1110,6 +1246,10 @@
                     TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
                     !hasAnyFlag(FLAG_STASHED_IN_APP_AUTO));
         }
+        if (hasAnyFlag(changedFlags, FLAG_IN_OVERVIEW | FLAG_IN_APP)) {
+            mControllers.runAfterInit(() -> mControllers.taskbarInsetsController
+                    .onTaskbarOrBubblebarWindowHeightOrInsetsChanged());
+        }
         mActivity.applyForciblyShownFlagWhileTransientTaskbarUnstashed(!isStashedInApp());
     }
 
@@ -1124,7 +1264,8 @@
      */
     public void setUpTaskbarSystemAction(boolean visible) {
         UI_HELPER_EXECUTOR.execute(() -> {
-            if (!visible || !DisplayController.isTransientTaskbar(mActivity)) {
+            if (!visible || !DisplayController.isTransientTaskbar(mActivity)
+                    || mActivity.isPhoneMode()) {
                 mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR);
                 mIsTaskbarSystemActionRegistered = false;
                 return;
@@ -1148,6 +1289,12 @@
      * Clean up on destroy from TaskbarControllers
      */
     public void onDestroy() {
+        // If the controller is destroyed before the animation finishes, we cancel the animation
+        // so that we don't finish the CUJ.
+        if (mAnimator != null) {
+            mAnimator.cancel();
+            mAnimator = null;
+        }
         UI_HELPER_EXECUTOR.execute(
                 () -> mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR));
     }
@@ -1181,7 +1328,7 @@
     /**
      * Attempts to start timer to auto hide the taskbar based on time.
      */
-    public void tryStartTaskbarTimeout() {
+    private void tryStartTaskbarTimeout() {
         if (!DisplayController.isTransientTaskbar(mActivity)
                 || mIsStashed
                 || mEnableBlockingTimeoutDuringTests) {
@@ -1209,6 +1356,11 @@
         updateAndAnimateTransientTaskbarForTimeout();
     }
 
+    @VisibleForTesting
+    Alarm getTimeoutAlarm() {
+        return mTimeoutAlarm;
+    }
+
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarStashController:");
@@ -1311,8 +1463,9 @@
                 mIsStashed = isStashed;
                 mLastStartedTransitionType = animationType;
 
+                boolean shouldDelayBackground = hasAnyFlag(FLAG_DELAY_TASKBAR_BG_TAG);
                 // This sets mAnimator.
-                createAnimToIsStashed(mIsStashed, duration, animationType,
+                createAnimToIsStashed(mIsStashed, duration, animationType, shouldDelayBackground,
                         computeTaskbarJankMonitorTag(changedFlags));
                 return mAnimator;
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
index 5b6fbef..17516f3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
@@ -25,7 +25,6 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
 
 /**
  * Utility class that contains the different taskbar thresholds logic.
@@ -39,10 +38,6 @@
 
     private static int getThreshold(Resources r, DeviceProfile dp, int thresholdDimen,
             int multiplierDimen) {
-        if (!FeatureFlags.ENABLE_DYNAMIC_TASKBAR_THRESHOLDS.get()) {
-            return r.getDimensionPixelSize(thresholdDimen);
-        }
-
         float landscapeScreenHeight = dp.isLandscape ? dp.heightPx : dp.widthPx;
         float screenPart = (landscapeScreenHeight * SCREEN_UNITS);
         float defaultDp = dpiFromPx(screenPart, DisplayMetrics.DENSITY_DEVICE_STABLE);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
index 144c0c2..5a5d6d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -95,7 +95,8 @@
         mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
         mControllers.bubbleControllers.ifPresent(controllers -> {
             controllers.bubbleBarViewController.setTranslationYForSwipe(transY);
-            controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY);
+            controllers.bubbleStashedHandleViewController.ifPresent(
+                    controller -> controller.setTranslationYForSwipe(transY));
         });
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index a2278ec..f7f5cf6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
-import static com.android.quickstep.OverviewCommandHelper.TYPE_HIDE;
 
 import android.content.Intent;
 import android.graphics.drawable.BitmapDrawable;
@@ -37,9 +36,11 @@
 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;
+import com.android.quickstep.OverviewCommandHelper.CommandType;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
@@ -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,12 +57,14 @@
 /**
  * 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.
     protected TaskbarControllers mControllers;
 
+    protected boolean mSkipLauncherVisibilityChange;
+
     @CallSuper
     protected void init(TaskbarControllers taskbarControllers) {
         mControllers = taskbarControllers;
@@ -96,14 +100,7 @@
     }
 
     /** Called when an icon is launched. */
-    @CallSuper
-    public void onTaskbarIconLaunched(ItemInfo item) {
-        // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately instead of
-        // waiting for onPause, to reduce potential visual noise during the app open transition.
-        if (mControllers.taskbarStashController == null) return;
-        mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true);
-        mControllers.taskbarStashController.applyState();
-    }
+    public void onTaskbarIconLaunched(ItemInfo item) { }
 
     public View getRootView() {
         return mControllers.taskbarActivityContext.getDragLayer();
@@ -121,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();
@@ -174,11 +173,11 @@
                 || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
     }
 
-    /** Checks if the given {@link MotionEvent} is over the bubble bar stash handle. */
-    public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) {
+    /** Checks if the given {@link MotionEvent} is over the bubble bar views. */
+    public boolean isEventOverBubbleBarViews(MotionEvent ev) {
         return mControllers.bubbleControllers.map(
                 bubbleControllers ->
-                        bubbleControllers.bubbleStashController.isEventOverStashHandle(ev))
+                        bubbleControllers.bubbleStashController.isEventOverBubbleBarViews(ev))
                 .orElse(false);
     }
 
@@ -230,7 +229,7 @@
         }
 
         recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback(
-                Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+                Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()),
                 false /* findExactPairMatch */,
                 foundTasks -> {
                     @Nullable Task foundTask = foundTasks[0];
@@ -247,6 +246,13 @@
      * Uses the clicked Taskbar icon to launch a second app for splitscreen.
      */
     public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) {
+        // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately
+        // to reduce potential visual noise during the app open transition.
+        if (mControllers.taskbarStashController != null) {
+            mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true);
+            mControllers.taskbarStashController.applyState();
+        }
+
         RecentsView recents = getRecentsView();
         recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
                 Collections.singletonList(info.getComponentKey()),
@@ -392,7 +398,7 @@
         if (overviewCommandHelper == null) {
             return;
         }
-        overviewCommandHelper.addCommand(TYPE_HIDE);
+        overviewCommandHelper.addCommand(CommandType.HIDE);
     }
 
     /**
@@ -418,4 +424,34 @@
     public void setUserIsNotGoingHome(boolean isNotGoingHome) {
         mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome);
     }
+
+    /**
+     * Sets whether to prevent taskbar from reacting to launcher visibility during the recents
+     * transition animation.
+     */
+    public void setSkipLauncherVisibilityChange(boolean skip) {
+        mSkipLauncherVisibilityChange = skip;
+    }
+
+    /** Sets whether the hotseat is stashed */
+    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() {
+    }
+
+    /**
+     * Called when we want to unstash taskbar when user performs swipes up gesture.
+     */
+    public void onSwipeToUnstashTaskbar() {
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c42d6c6..55bcb23 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -22,10 +22,8 @@
 import static com.android.launcher3.Flags.enableRecentsInTaskbar;
 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.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -39,18 +37,16 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
-import androidx.annotation.DimenRes;
-import androidx.annotation.DrawableRes;
 import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BubbleTextView;
 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;
@@ -62,17 +58,18 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer;
+import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
 import com.android.launcher3.util.DisplayController;
 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.DeviceConfigWrapper;
-import com.android.quickstep.util.AssistStateManager;
-import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Predicate;
 
@@ -92,6 +89,7 @@
     private final boolean mIsRtl;
 
     private final TaskbarActivityContext mActivityContext;
+    @Nullable private BubbleBarLocation mBubbleBarLocation = null;
 
     // Initialized in init.
     private TaskbarViewCallbacks mControllerCallbacks;
@@ -99,16 +97,22 @@
     private View.OnLongClickListener mIconLongClickListener;
 
     // Only non-null when the corresponding Folder is open.
-    private @Nullable FolderIcon mLeaveBehindFolderIcon;
+    @Nullable private FolderIcon mLeaveBehindFolderIcon;
 
     // Only non-null when device supports having an All Apps button.
-    private @Nullable IconButtonView mAllAppsButton;
-    private Runnable mAllAppsTouchRunnable;
-    private long mAllAppsButtonTouchDelayMs;
-    private boolean mAllAppsTouchTriggered;
+    private final TaskbarAllAppsButtonContainer mAllAppsButtonContainer;
 
-    // Only non-null when device supports having an All Apps button.
-    private @Nullable IconButtonView mTaskbarDivider;
+    // 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 TaskbarOverflowView mTaskbarOverflowView;
+
+    /**
+     * Whether the divider is between Hotseat icons and Recents,
+     * instead of between All Apps button and Hotseat.
+     */
+    private boolean mAddedDividerForRecents;
 
     private final View mQsb;
 
@@ -116,6 +120,10 @@
 
     private boolean mShouldTryStartAlign;
 
+    private final int mMaxNumIcons;
+
+    private final int mAllAppsButtonTranslationOffset;
+
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -135,8 +143,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);
 
@@ -166,55 +172,63 @@
         // Needed to draw folder leave-behind when opening one.
         setWillNotDraw(false);
 
-        mAllAppsButton = (IconButtonView) LayoutInflater.from(context)
-                .inflate(R.layout.taskbar_all_apps_button, this, false);
-        mAllAppsButton.setIconDrawable(resources.getDrawable(
-                getAllAppsButton(isTransientTaskbar)));
-        mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
-        mAllAppsButton.setForegroundTint(
-                mActivityContext.getColor(R.color.all_apps_button_color));
+        mAllAppsButtonContainer = new TaskbarAllAppsButtonContainer(context);
+        mAllAppsButtonTranslationOffset =  (int) getResources().getDimension(
+                mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(isTransientTaskbar()));
 
         if (enableTaskbarPinning() || enableRecentsInTaskbar()) {
-            mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
-                    R.layout.taskbar_divider,
-                    this, false);
-            mTaskbarDivider.setIconDrawable(
-                    resources.getDrawable(R.drawable.taskbar_divider_button));
-            mTaskbarDivider.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+            mTaskbarDividerContainer = new TaskbarDividerContainer(context);
+        }
+
+        if (Flags.taskbarOverflow()) {
+            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);
 
-        // Default long press (touch) delay = 400ms
-        mAllAppsButtonTouchDelayMs = ViewConfiguration.getLongPressTimeout();
+        mMaxNumIcons = calculateMaxNumIcons();
     }
 
-    @DrawableRes
-    private int getAllAppsButton(boolean isTransientTaskbar) {
-        boolean shouldSelectTransientIcon =
-                (isTransientTaskbar || enableTaskbarPinning())
-                && !mActivityContext.isThreeButtonNav();
-        if (ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
-            return shouldSelectTransientIcon
-                    ? R.drawable.ic_transient_taskbar_all_apps_search_button
-                    : R.drawable.ic_taskbar_all_apps_search_button;
-        } else {
-            return shouldSelectTransientIcon
-                    ? R.drawable.ic_transient_taskbar_all_apps_button
-                    : R.drawable.ic_taskbar_all_apps_button;
-        }
-    }
+    /**
+     * @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);
 
-    @DimenRes
-    public int getAllAppsButtonTranslationXOffset(boolean isTransientTaskbar) {
-        if (isTransientTaskbar) {
-            return R.dimen.transient_taskbar_all_apps_button_translation_x_offset;
-        } else {
-            return ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()
-                    ? R.dimen.taskbar_all_apps_search_button_translation_x_offset
-                    : R.dimen.taskbar_all_apps_button_translation_x_offset;
+        // 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, deviceProfile.hotseatBarEndOffset);
+        availableWidth -= Math.max(defaultEdgeMargin,
+                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;
     }
 
     @Override
@@ -240,18 +254,40 @@
 
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
-        mShouldTryStartAlign = mActivityContext.isThreeButtonNav() && dp.startAlignTaskbar;
+        mShouldTryStartAlign = mActivityContext.shouldStartAlignTaskbar();
     }
 
     @Override
     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
         if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
-            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title));
+            announceTaskbarShown();
         } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
-            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
+            announceTaskbarHidden();
         }
         return super.performAccessibilityActionInternal(action, arguments);
+    }
 
+    private void announceTaskbarShown() {
+        BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
+        if (bubbleBarLocation == null) {
+            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title));
+        } else if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
+            announceForAccessibility(
+                    mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_left_title));
+        } else {
+            announceForAccessibility(
+                    mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_right_title));
+        }
+    }
+
+    private void announceTaskbarHidden() {
+        BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
+        if (bubbleBarLocation == null) {
+            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
+        } else {
+            announceForAccessibility(
+                    mContext.getString(R.string.taskbar_a11y_hidden_with_bubbles_title));
+        }
     }
 
     protected void announceAccessibilityChanges() {
@@ -277,27 +313,20 @@
         mIconClickListener = mControllerCallbacks.getIconOnClickListener();
         mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
 
-        if (mAllAppsButton != null) {
-            mAllAppsButton.setOnClickListener(this::onAllAppsButtonClick);
-            mAllAppsButton.setOnLongClickListener(this::onAllAppsButtonLongClick);
-            mAllAppsButton.setOnTouchListener(this::onAllAppsButtonTouch);
-            mAllAppsButton.setHapticFeedbackEnabled(
-                    mControllerCallbacks.isAllAppsButtonHapticFeedbackEnabled());
-            mAllAppsTouchRunnable = () -> {
-                mControllerCallbacks.triggerAllAppsButtonLongClick();
-                mAllAppsTouchTriggered = true;
-            };
-            AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(mContext);
-            if (DeviceConfigWrapper.get().getCustomLpaaThresholds()
-                    && assistStateManager.getLPNHDurationMillis().isPresent()) {
-                mAllAppsButtonTouchDelayMs = assistStateManager.getLPNHDurationMillis().get();
-            }
+        mAllAppsButtonContainer.setUpCallbacks(callbacks);
+        if (mTaskbarDividerContainer != null
+                && mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup()) {
+            mTaskbarDividerContainer.setUpCallbacks(callbacks);
         }
-        if (mTaskbarDivider != null && !mActivityContext.isThreeButtonNav()) {
-            mTaskbarDivider.setOnLongClickListener(
-                    mControllerCallbacks.getTaskbarDividerLongClickListener());
-            mTaskbarDivider.setOnTouchListener(
-                    mControllerCallbacks.getTaskbarDividerRightClickListener());
+        if (mTaskbarOverflowView != null) {
+            mTaskbarOverflowView.setOnClickListener(
+                    mControllerCallbacks.getOverflowOnClickListener());
+            mTaskbarOverflowView.setOnLongClickListener(
+                    mControllerCallbacks.getOverflowOnLongClickListener());
+        }
+        if (Flags.showTaskbarPinningPopupFromAnywhere()
+                && mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup()) {
+            setOnTouchListener(mControllerCallbacks.getTaskbarTouchListener());
         }
     }
 
@@ -317,14 +346,15 @@
     protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
         int nextViewIndex = 0;
         int numViewsAnimated = 0;
-        boolean addedDividerForRecents = false;
+        mAddedDividerForRecents = false;
 
-        if (mAllAppsButton != null) {
-            removeView(mAllAppsButton);
+        removeView(mAllAppsButtonContainer);
 
-            if (mTaskbarDivider != null) {
-                removeView(mTaskbarDivider);
-            }
+        if (mTaskbarDividerContainer != null) {
+            removeView(mTaskbarDividerContainer);
+        }
+        if (mTaskbarOverflowView != null) {
+            removeView(mTaskbarOverflowView);
         }
         removeView(mQsb);
 
@@ -410,18 +440,64 @@
             nextViewIndex++;
         }
 
-        if (mTaskbarDivider != null && !recentTasks.isEmpty()) {
-            addView(mTaskbarDivider, nextViewIndex++);
-            addedDividerForRecents = true;
+        if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) {
+            addView(mTaskbarDividerContainer, nextViewIndex++);
+            mAddedDividerForRecents = true;
+        }
+
+        // At this point, the all apps button has not been added as a child view, but needs to be
+        // accounted for when comparing current icon count to max number of icons.
+        int nonTaskIconsToBeAdded = 1;
+
+        boolean supportsOverflow = Flags.taskbarOverflow();
+        int overflowSize = 0;
+        if (supportsOverflow) {
+            int numberOfSupportedRecents = 0;
+            for (GroupTask task : recentTasks) {
+                // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+                if (!task.supportsMultipleTasks()) {
+                    ++numberOfSupportedRecents;
+                }
+            }
+
+            overflowSize =
+                    nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded - mMaxNumIcons;
+            if (overflowSize > 0 && mTaskbarOverflowView != null) {
+                addView(mTaskbarOverflowView, nextViewIndex++);
+            } else if (mTaskbarOverflowView != null) {
+                mTaskbarOverflowView.clearItems();
+            }
+        }
+
+        List<Task> overflownTasks = null;
+        // An extra item needs to be added to overflow button to account for the space taken up by
+        // the overflow button.
+        final int itemsToAddToOverflow = overflowSize > 0 ? overflowSize + 1 : 0;
+        if (overflowSize > 0) {
+            overflownTasks = new ArrayList<Task>(itemsToAddToOverflow);
         }
 
         // Add Recent/Running icons.
         for (GroupTask task : recentTasks) {
+            if (mTaskbarOverflowView != null && overflownTasks != null
+                    && overflownTasks.size() < itemsToAddToOverflow) {
+                // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+                if (task.supportsMultipleTasks()) {
+                    continue;
+                }
+
+                overflownTasks.add(task.task1);
+                if (overflownTasks.size() == itemsToAddToOverflow) {
+                    mTaskbarOverflowView.setItems(overflownTasks);
+                }
+                continue;
+            }
+
             // Replace any Recent views with the appropriate type if it's not already that type.
             final int expectedLayoutResId;
             boolean isCollection = false;
-            if (task.hasMultipleTasks()) {
-                if (task instanceof DesktopTask) {
+            if (task.supportsMultipleTasks()) {
+                if (task.taskViewType == TaskViewType.DESKTOP) {
                     // TODO(b/316004172): use Desktop tile layout.
                     expectedLayoutResId = -1;
                 } else {
@@ -475,14 +551,16 @@
             removeAndRecycle(getChildAt(nextViewIndex));
         }
 
-        if (mAllAppsButton != null) {
-            addView(mAllAppsButton, mIsRtl ? getChildCount() : 0);
+        addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0);
 
-            // If there are no recent tasks, add divider after All Apps (unless it's the only view).
-            if (!addedDividerForRecents && mTaskbarDivider != null && getChildCount() > 1) {
-                addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1);
-            }
+        // If there are no recent tasks, add divider after All Apps (unless it's the only view).
+        if (!mAddedDividerForRecents
+                && mTaskbarDividerContainer != null
+                && getChildCount() > 1) {
+            addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1);
         }
+
+
         if (mActivityContext.getDeviceProfile().isQsbInline) {
             addView(mQsb, mIsRtl ? getChildCount() : 0);
             // Always set QSB to invisible after re-adding.
@@ -529,39 +607,60 @@
         icon.setOnHoverListener(mControllerCallbacks.getIconOnHoverListener(icon));
     }
 
+    /** Updates taskbar icons accordingly to the new bubble bar location. */
+    public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
+        if (mBubbleBarLocation == location) return;
+        mBubbleBarLocation = location;
+        requestLayout();
+    }
+
+    /**
+     * Returns translation X for the taskbar icons for provided {@link BubbleBarLocation}. If the
+     * bubble bar is not enabled, or location of the bubble bar is the same, or taskbar is not start
+     * aligned - returns 0.
+     */
+    public float getTranslationXForBubbleBarPosition(BubbleBarLocation location) {
+        if (!mControllerCallbacks.isBubbleBarEnabledInPersistentTaskbar()
+                || location == mBubbleBarLocation
+                || !mActivityContext.shouldStartAlignTaskbar()
+        ) {
+            return 0;
+        }
+        Rect iconsBounds = getIconLayoutBounds();
+        return getTaskBarIconsEndForBubbleBarLocation(location) - iconsBounds.right;
+    }
+
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        int count = getChildCount();
-        DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
         int spaceNeeded = getIconLayoutWidth();
-        int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
         boolean layoutRtl = isLayoutRtl();
-        int centerAlignIconEnd = right - (right - left - spaceNeeded) / 2;
-        int iconEnd;
-
+        DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+        int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
+        int centerAlignIconEnd = (right + left + spaceNeeded) / 2;
+        int iconEnd = centerAlignIconEnd;
         if (mShouldTryStartAlign) {
-            // Taskbar is aligned to the start
             int startSpacingPx = deviceProfile.inlineNavButtonsEndSpacingPx;
-
-            if (layoutRtl) {
-                iconEnd = right - startSpacingPx;
+            if (mControllerCallbacks.isBubbleBarEnabledInPersistentTaskbar()
+                    && mBubbleBarLocation != null
+                    && mActivityContext.shouldStartAlignTaskbar()) {
+                iconEnd = (int) getTaskBarIconsEndForBubbleBarLocation(mBubbleBarLocation);
             } else {
-                iconEnd = startSpacingPx + spaceNeeded;
+                if (layoutRtl) {
+                    iconEnd = right - startSpacingPx;
+                } else {
+                    iconEnd = startSpacingPx + spaceNeeded;
+                }
+                boolean needMoreSpaceForNav = layoutRtl
+                        ? navSpaceNeeded > (iconEnd - spaceNeeded)
+                        : iconEnd > (right - navSpaceNeeded);
+                if (needMoreSpaceForNav) {
+                    // Add offset to account for nav bar when taskbar is centered
+                    int offset = layoutRtl
+                            ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded)
+                            : (right - navSpaceNeeded) - centerAlignIconEnd;
+                    iconEnd = centerAlignIconEnd + offset;
+                }
             }
-        } else {
-            iconEnd = centerAlignIconEnd;
-        }
-
-        boolean needMoreSpaceForNav = layoutRtl
-                ? navSpaceNeeded > (iconEnd - spaceNeeded)
-                : iconEnd > (right - navSpaceNeeded);
-        if (needMoreSpaceForNav) {
-            // Add offset to account for nav bar when taskbar is centered
-            int offset = layoutRtl
-                    ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded)
-                    : (right - navSpaceNeeded) - centerAlignIconEnd;
-
-            iconEnd = centerAlignIconEnd + offset;
         }
 
         // Currently, we support only one device with display cutout and we only are concern about
@@ -593,6 +692,16 @@
         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);
             if (child == mQsb) {
@@ -608,7 +717,7 @@
                 int qsbTop = (bottom - top - deviceProfile.hotseatQsbHeight) / 2;
                 int qsbBottom = qsbTop + deviceProfile.hotseatQsbHeight;
                 child.layout(qsbStart, qsbTop, qsbEnd, qsbBottom);
-            } else if (child == mTaskbarDivider) {
+            } else if (child == mTaskbarDividerContainer) {
                 iconEnd += mItemMarginLeftRight;
                 int iconStart = iconEnd - mIconTouchSize;
                 child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom);
@@ -623,6 +732,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;
@@ -646,19 +764,32 @@
         return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
     }
 
+    /**
+     * Gets visual bounds of the taskbar view. The visual bounds correspond to the taskbar touch
+     * area, rather than layout placement in the parent view.
+     */
+    public Rect getIconLayoutVisualBounds() {
+        return new Rect(mIconLayoutBounds);
+    }
+
+    /** Gets taskbar layout bounds in parent view. */
     public Rect getIconLayoutBounds() {
-        return mIconLayoutBounds;
+        Rect actualBounds = new Rect(mIconLayoutBounds);
+        actualBounds.top = getTop();
+        actualBounds.bottom = getBottom();
+        return actualBounds;
     }
 
     /**
      * 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);
 
@@ -667,17 +798,28 @@
             // All Apps icon, divider icon, and first app icon in taskbar
             iconLayoutBoundsWidth -= mItemMarginLeftRight * 4;
         }
+
+        // The all apps button container gets offset horizontally, reducing the overall taskbar
+        // view size.
+        iconLayoutBoundsWidth -= mAllAppsButtonTranslationOffset;
+
         return iconLayoutBoundsWidth;
     }
 
     /**
-     * Returns the app icons currently shown in the taskbar.
+     * Returns the app icons currently shown in the taskbar. The returned list does not include qsb,
+     * but it includes all apps button and icon divider views.
      */
     public View[] getIconViews() {
         final int count = getChildCount();
-        View[] icons = new View[count];
+        if (count == 0) {
+            return new View[0];
+        }
+        View[] icons = new View[count - (mActivityContext.getDeviceProfile().isQsbInline ? 1 : 0)];
+        int insertionPoint = 0;
         for (int i = 0; i < count; i++) {
-            icons[i] = getChildAt(i);
+            if (getChildAt(i)  == mQsb) continue;
+            icons[insertionPoint++] = getChildAt(i);
         }
         return icons;
     }
@@ -685,17 +827,32 @@
     /**
      * Returns the all apps button in the taskbar.
      */
-    @Nullable
-    public View getAllAppsButtonView() {
-        return mAllAppsButton;
+    public TaskbarAllAppsButtonContainer getAllAppsButtonContainer() {
+        return mAllAppsButtonContainer;
     }
 
     /**
      * Returns the taskbar divider in the taskbar.
      */
     @Nullable
-    public View getTaskbarDividerView() {
-        return mTaskbarDivider;
+    public TaskbarDividerContainer getTaskbarDividerViewContainer() {
+        return mTaskbarDividerContainer;
+    }
+
+    /**
+     * Returns the taskbar overflow view in the taskbar.
+     */
+    @Nullable
+    public TaskbarOverflowView getTaskbarOverflowView() {
+        return mTaskbarOverflowView;
+    }
+
+    /**
+     * Returns whether the divider is between Hotseat icons and Recents,
+     * instead of between All Apps button and Hotseat.
+     */
+    public boolean isDividerForRecents() {
+        return mAddedDividerForRecents;
     }
 
     /**
@@ -744,12 +901,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) {
@@ -783,48 +953,21 @@
                 }
             }
         }
-        return mAllAppsButton;
+        return mAllAppsButtonContainer;
     }
 
-    private boolean onAllAppsButtonTouch(View view, MotionEvent ev) {
-        switch (ev.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                mAllAppsTouchTriggered = false;
-                MAIN_EXECUTOR.getHandler().postDelayed(
-                        mAllAppsTouchRunnable, mAllAppsButtonTouchDelayMs);
-                break;
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL:
-                cancelAllAppsButtonTouch();
+    /**
+     * This method only works for bubble bar enabled in persistent task bar and the taskbar is start
+     * aligned.
+     */
+    private float getTaskBarIconsEndForBubbleBarLocation(BubbleBarLocation location) {
+        DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+        boolean navbarOnRight = location.isOnLeft(isLayoutRtl());
+        int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
+        if (navbarOnRight) {
+            return getWidth() - navSpaceNeeded;
+        } else {
+            return navSpaceNeeded + getIconLayoutWidth();
         }
-        return false;
-    }
-
-    private void cancelAllAppsButtonTouch() {
-        MAIN_EXECUTOR.getHandler().removeCallbacks(mAllAppsTouchRunnable);
-        // ACTION_UP is first triggered, then click listener / long-click listener is triggered on
-        // the next frame, so we need to post twice and delay the reset.
-        if (mAllAppsButton != null) {
-            mAllAppsButton.post(() -> {
-                mAllAppsButton.post(() -> {
-                    mAllAppsTouchTriggered = false;
-                });
-            });
-        }
-    }
-
-    private void onAllAppsButtonClick(View view) {
-        if (!mAllAppsTouchTriggered) {
-            mControllerCallbacks.triggerAllAppsButtonClick(view);
-        }
-    }
-
-    // Handle long click from Switch Access and Voice Access
-    private boolean onAllAppsButtonLongClick(View view) {
-        if (!MAIN_EXECUTOR.getHandler().hasCallbacks(mAllAppsTouchRunnable)
-                && !mAllAppsTouchTriggered) {
-            mControllerCallbacks.triggerAllAppsButtonLongClick();
-        }
-        return true;
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 3c646cb..834f92e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -19,12 +19,22 @@
 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;
 
 /**
  * Callbacks for {@link TaskbarView} to interact with its controller.
@@ -34,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() {
@@ -47,7 +59,7 @@
     }
 
     /** Trigger All Apps button click action. */
-    protected void triggerAllAppsButtonClick(View v) {
+    public void triggerAllAppsButtonClick(View v) {
         InteractionJankMonitorWrapper.begin(v, Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS,
                 /* tag= */ "TASKBAR_BUTTON");
         mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP);
@@ -55,17 +67,23 @@
     }
 
     /** Trigger All Apps button long click action. */
-    protected void triggerAllAppsButtonLongClick() {
+    public void triggerAllAppsButtonLongClick() {
         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;
         };
     }
@@ -74,7 +92,7 @@
         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;
@@ -104,4 +122,89 @@
         mControllers.taskbarScrimViewController.onTaskbarVisibilityChanged(
                 mTaskbarView.getVisibility());
     }
+
+    /**
+     * Get current location of bubble bar, if it is visible.
+     * Returns {@code null} if bubble bar is not shown.
+     */
+    @Nullable
+    public BubbleBarLocation getBubbleBarLocationIfVisible() {
+        BubbleBarViewController bubbleBarViewController =
+                mControllers.bubbleControllers.map(c -> c.bubbleBarViewController).orElse(null);
+        if (bubbleBarViewController != null && bubbleBarViewController.isBubbleBarVisible()) {
+            return bubbleBarViewController.getBubbleBarLocation();
+        }
+        return null;
+    }
+
+    /** Returns true if bubble bar controllers present and enabled in persistent taskbar. */
+    public boolean isBubbleBarEnabledInPersistentTaskbar() {
+        return Flags.enableBubbleBarInPersistentTaskBar()
+                && mControllers.bubbleControllers.isPresent();
+    }
+
+    /** Returns on click listener for the taskbar overflow view. */
+    public View.OnClickListener getOverflowOnClickListener() {
+        return new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                toggleKeyboardQuickSwitchView();
+            }
+        };
+    }
+
+    /** Returns on long click listener for the taskbar overflow view. */
+    public View.OnLongClickListener getOverflowOnLongClickListener() {
+        return new View.OnLongClickListener() {
+            @Override
+            public boolean onLongClick(View v) {
+                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 527e3a3..494c472 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;
@@ -29,7 +30,10 @@
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT;
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
+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.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
+import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_NAV_BAR_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_PINNING_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_REVEAL_ANIM;
@@ -46,6 +50,7 @@
 import android.view.animation.Interpolator;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.view.OneShotPreDrawListener;
 
 import com.android.app.animation.Interpolators;
@@ -64,24 +69,29 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.TaskItemInfo;
+import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.IconButtonView;
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.wm.shell.Flags;
+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;
 
 /**
  * Handles properties/data collection, then passes the results to TaskbarView to render.
  */
-public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController {
+public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController,
+        BubbleBarController.BubbleBarLocationListener {
 
     private static final String TAG = "TaskbarViewController";
 
@@ -94,7 +104,17 @@
     public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4;
     public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 5;
     public static final int ALPHA_INDEX_SMALL_SCREEN = 6;
-    private static final int NUM_ALPHA_CHANNELS = 7;
+
+    public static final int ALPHA_INDEX_BUBBLE_BAR = 7;
+    private static final int NUM_ALPHA_CHANNELS = 8;
+
+    /** Only used for animation purposes, to position the divider between two item indices. */
+    public static final float DIVIDER_VIEW_POSITION_OFFSET = 0.5f;
+
+    /** Used if an unexpected edge case is hit in {@link #getPositionInHotseat}. */
+    private static final float ERROR_POSITION_IN_HOTSEAT_NOT_FOUND = -100;
+
+    private static boolean sEnableModelLoadingForTests = true;
 
     private final TaskbarActivityContext mActivity;
     private final TaskbarView mTaskbarView;
@@ -111,13 +131,17 @@
     private final AnimatedFloat mTaskbarIconTranslationXForPinning = new AnimatedFloat(
             this::updateTaskbarIconTranslationXForPinning);
 
+    private final AnimatedFloat mIconsTranslationXForNavbar = new AnimatedFloat(
+            this::updateTranslationXForNavBar);
+
+    @Nullable
+    private Animator mTaskbarShiftXAnim;
+    @Nullable
+    private BubbleBarLocation mCurrentBubbleBarLocation;
+
     private final AnimatedFloat mTaskbarIconTranslationYForPinning = new AnimatedFloat(
             this::updateTranslationY);
 
-    private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener =
-            (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
-                    -> updateTaskbarIconTranslationXForPinning();
-
 
     private AnimatedFloat mTaskbarNavButtonTranslationY;
     private AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay;
@@ -132,12 +156,21 @@
     // Initialized in init.
     private TaskbarControllers mControllers;
 
+    private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener =
+            (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                updateTaskbarIconTranslationXForPinning();
+                if (BubbleBarController.isBubbleBarEnabled()) {
+                    mControllers.navbarButtonsViewController.onLayoutsUpdated();
+                }
+            };
+
     // Animation to align icons with Launcher, created lazily. This allows the controller to be
     // active only during the animation and does not need to worry about layout changes.
     private AnimatorPlaybackController mIconAlignControllerLazy = null;
     private Runnable mOnControllerPreCreateCallback = NO_OP;
 
     // Stored here as signals to determine if the mIconAlignController needs to be recreated.
+    private boolean mIsIconAlignedWithHotseat;
     private boolean mIsHotseatIconOnTopWhenAligned;
     private boolean mIsStashed;
 
@@ -192,9 +225,10 @@
         mTaskbarIconTranslationXForPinning.updateValue(pinningValue);
 
         mModelCallbacks.init(controllers);
-        if (mActivity.isUserSetupComplete()) {
+        if (mActivity.isUserSetupComplete() && sEnableModelLoadingForTests) {
             // Only load the callbacks if user setup is completed
-            LauncherAppState.getInstance(mActivity).getModel().addCallbacksAndLoad(mModelCallbacks);
+            controllers.runAfterInit(() -> LauncherAppState.getInstance(mActivity).getModel()
+                    .addCallbacksAndLoad(mModelCallbacks));
         }
         mTaskbarNavButtonTranslationY =
                 controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY();
@@ -206,14 +240,62 @@
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
             // This gets modified in NavbarButtonsViewController, but the initial value it reads
             // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here
-            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN)
-                    .animateToValue(mActivity.isPhoneButtonNavMode() ? 0 : 1).start();
+            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN).setValue(
+                    mActivity.isPhoneMode() ? 0 : 1);
         }
         if (enableTaskbarPinning()) {
             mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener);
         }
     }
 
+    /** Adjusts start aligned taskbar layout accordingly to the bubble bar position. */
+    @Override
+    public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
+        updateCurrentBubbleBarLocation(location);
+        if (!shouldMoveTaskbarOnBubbleBarLocationUpdate()) return;
+        cancelTaskbarShiftAnimation();
+        // reset translation x, taskbar will position icons with the updated location
+        mIconsTranslationXForNavbar.updateValue(0);
+        mTaskbarView.onBubbleBarLocationUpdated(location);
+    }
+
+    /** Animates start aligned taskbar accordingly to the bubble bar position. */
+    @Override
+    public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
+        if (!updateCurrentBubbleBarLocation(location)
+                || !shouldMoveTaskbarOnBubbleBarLocationUpdate()) {
+            return;
+        }
+        cancelTaskbarShiftAnimation();
+        float translationX = mTaskbarView.getTranslationXForBubbleBarPosition(location);
+        mTaskbarShiftXAnim = createTaskbarIconsShiftAnimator(translationX);
+        mTaskbarShiftXAnim.start();
+    }
+
+    /** Updates the mCurrentBubbleBarLocation, returns {@code} true if location is updated. */
+    private boolean updateCurrentBubbleBarLocation(BubbleBarLocation location) {
+        if (mCurrentBubbleBarLocation == location || location == null) {
+            return false;
+        } else {
+            mCurrentBubbleBarLocation = location;
+            return true;
+        }
+    }
+
+    /** Returns whether taskbar should be moved on the bubble bar location update. */
+    private boolean shouldMoveTaskbarOnBubbleBarLocationUpdate() {
+        return Flags.enableBubbleBarInPersistentTaskBar()
+                && mControllers.bubbleControllers.isPresent()
+                && mActivity.shouldStartAlignTaskbar()
+                && mActivity.isThreeButtonNav();
+    }
+
+    private void cancelTaskbarShiftAnimation() {
+        if (mTaskbarShiftXAnim != null) {
+            mTaskbarShiftXAnim.cancel();
+        }
+    }
+
     /**
      * Announcement for Accessibility when Taskbar stashes/unstashes.
      */
@@ -229,6 +311,13 @@
         mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
     }
 
+    /**
+     * Gets the taskbar {@link View.Visibility visibility}.
+     */
+    public int getTaskbarVisibility() {
+        return mTaskbarView.getVisibility();
+    }
+
     public boolean areIconsVisible() {
         return mTaskbarView.areIconsVisible();
     }
@@ -242,7 +331,8 @@
      */
     public void setRecentsButtonDisabled(boolean isDisabled) {
         // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha.
-        mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1);
+        mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).animateToValue(isDisabled ? 0 : 1)
+                .start();
     }
 
     /**
@@ -261,21 +351,20 @@
         OneShotPreDrawListener.add(mTaskbarView, listener);
     }
 
-    public Rect getIconLayoutBounds() {
-        return mTaskbarView.getIconLayoutBounds();
+    public Rect getIconLayoutVisualBounds() {
+        return mTaskbarView.getIconLayoutVisualBounds();
     }
 
-    public int getIconLayoutWidth() {
-        return mTaskbarView.getIconLayoutWidth();
+    public Rect getIconLayoutBounds() {
+        return mTaskbarView.getIconLayoutBounds();
     }
 
     public View[] getIconViews() {
         return mTaskbarView.getIconViews();
     }
 
-    @Nullable
     public View getAllAppsButtonView() {
-        return mTaskbarView.getAllAppsButtonView();
+        return mTaskbarView.getAllAppsButtonContainer();
     }
 
     public AnimatedFloat getTaskbarIconScaleForStash() {
@@ -342,13 +431,26 @@
         View[] iconViews = mTaskbarView.getIconViews();
         float scale = mTaskbarIconTranslationXForPinning.value;
         float transientTaskbarAllAppsOffset = mActivity.getResources().getDimension(
-                mTaskbarView.getAllAppsButtonTranslationXOffset(true));
+                mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(true));
         float persistentTaskbarAllAppsOffset = mActivity.getResources().getDimension(
-                mTaskbarView.getAllAppsButtonTranslationXOffset(false));
+                mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(false));
 
         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;
@@ -356,36 +458,30 @@
 
         if (mIsRtl) {
             allAppIconTranslateRange *= -1;
+            sizeDiffTranslationRange *= -1;
         }
 
         if (mActivity.isThreeButtonNav()) {
-            ((IconButtonView) mTaskbarView.getAllAppsButtonView())
+            mTaskbarView.getAllAppsButtonContainer()
                     .setTranslationXForTaskbarAllAppsIcon(allAppIconTranslateRange);
             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.getAllAppsButtonView())) {
-                ((IconButtonView) iconView).setTranslationXForTaskbarAllAppsIcon(
+            if (iconView.equals(mTaskbarView.getAllAppsButtonContainer())) {
+                mTaskbarView.getAllAppsButtonContainer().setTranslationXForTaskbarAllAppsIcon(
                         allAppIconTranslateRange);
             }
         }
@@ -395,18 +491,14 @@
      * Calculates visual taskbar view width.
      */
     public float getCurrentVisualTaskbarWidth() {
-        if (mTaskbarView.getIconViews().length == 0) {
+        View[] iconViews = mTaskbarView.getIconViews();
+        if (iconViews.length == 0) {
             return 0;
         }
 
-        View[] iconViews = mTaskbarView.getIconViews();
+        float left = iconViews[0].getX();
 
-        int leftIndex = mActivity.getDeviceProfile().isQsbInline && !mIsRtl ? 1 : 0;
-        int rightIndex = mActivity.getDeviceProfile().isQsbInline && mIsRtl
-                ? iconViews.length - 2
-                : iconViews.length - 1;
-
-        float left = iconViews[leftIndex].getX();
+        int rightIndex = iconViews.length - 1;
         float right = iconViews[rightIndex].getRight() + iconViews[rightIndex].getTranslationX();
 
         return right - left + (2 * mTaskbarLeftRightMargin);
@@ -436,6 +528,17 @@
                 + mTaskbarIconTranslationYForSpringOnStash);
     }
 
+    private void updateTranslationXForNavBar() {
+        View[] iconViews = mTaskbarView.getIconViews();
+        float translationX = mIconsTranslationXForNavbar.value;
+        for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) {
+            View iconView = iconViews[iconIndex];
+            MultiTranslateDelegate translateDelegate =
+                    ((Reorderable) iconView).getTranslateDelegate();
+            translateDelegate.getTranslationX(INDEX_NAV_BAR_ANIM).setValue(translationX);
+        }
+    }
+
     /**
      * Computes translation y for taskbar pinning.
      */
@@ -451,14 +554,14 @@
         if (mControllers.getSharedState().startTaskbarVariantIsTransient) {
             float transY =
                     mTransientTaskbarDp.taskbarBottomMargin + (mTransientTaskbarDp.taskbarHeight
-                            - mTaskbarView.getIconLayoutBounds().bottom)
+                            - mTaskbarView.getIconLayoutVisualBounds().bottom)
                             - (mPersistentTaskbarDp.taskbarHeight
                                     - mTransientTaskbarDp.taskbarIconSize) / 2f;
             taskbarIconTranslationYForPinningValue = mapRange(scale, 0f, transY);
         } else {
             float transY =
                     -mTransientTaskbarDp.taskbarBottomMargin + (mPersistentTaskbarDp.taskbarHeight
-                            - mTaskbarView.getIconLayoutBounds().bottom)
+                            - mTaskbarView.getIconLayoutVisualBounds().bottom)
                             - (mTransientTaskbarDp.taskbarHeight
                                     - mTransientTaskbarDp.taskbarIconSize) / 2f;
             taskbarIconTranslationYForPinningValue = mapRange(scale, transY, 0f);
@@ -513,43 +616,48 @@
     }
 
     public View getTaskbarDividerView() {
-        return mTaskbarView.getTaskbarDividerView();
+        return mTaskbarView.getTaskbarDividerViewContainer();
     }
 
     /**
      * 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;
     }
@@ -650,15 +758,17 @@
             mIconAlignControllerLazy = null;
             return;
         }
-
         boolean isHotseatIconOnTopWhenAligned =
                 mControllers.uiController.isHotseatIconOnTopWhenAligned();
+        boolean isIconAlignedWithHotseat = mControllers.uiController.isIconAlignedWithHotseat();
         boolean isStashed = mControllers.taskbarStashController.isStashed();
-        // Re-create animation when mIsHotseatIconOnTopWhenAligned or mIsStashed changes.
+        // Re-create animation when any of these values change.
         if (mIconAlignControllerLazy == null
                 || mIsHotseatIconOnTopWhenAligned != isHotseatIconOnTopWhenAligned
+                || mIsIconAlignedWithHotseat != isIconAlignedWithHotseat
                 || mIsStashed != isStashed) {
             mIsHotseatIconOnTopWhenAligned = isHotseatIconOnTopWhenAligned;
+            mIsIconAlignedWithHotseat = isIconAlignedWithHotseat;
             mIsStashed = isStashed;
             mIconAlignControllerLazy = createIconAlignmentController(launcherDp);
         }
@@ -710,17 +820,31 @@
                 ? mTransientTaskbarDp.taskbarBottomMargin
                 : 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.getAllAppsButtonView();
-            boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerView();
+            boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonContainer();
+            boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerViewContainer();
+            boolean isTaskbarOverflowView = child == mTaskbarView.getTaskbarOverflowView();
+            boolean isRecentTask = child.getTag() instanceof GroupTask;
+            // TODO(b/343522351): show recents on the home screen.
+            final boolean isRecentsInHotseat = false;
             if (!mIsHotseatIconOnTopWhenAligned) {
                 // When going to home, the EMPHASIZED interpolator in TaskbarLauncherStateController
                 // plays iconAlignment to 1 really fast, therefore moving the fading towards the end
                 // to avoid icons disappearing rather than fading out visually.
                 setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0.8f, 1f));
-            } else if ((isAllAppsButton && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get())
-                    || (isTaskbarDividerView && enableTaskbarPinning())) {
+            } else if ((isAllAppsButton && !FeatureFlags.enableAllAppsButtonInHotseat())
+                    || (isTaskbarDividerView && enableTaskbarPinning())
+                    || (isRecentTask && !isRecentsInHotseat)
+                    || isTaskbarOverflowView) {
                 if (!isToHome
                         && mIsHotseatIconOnTopWhenAligned
                         && mIsStashed) {
@@ -739,16 +863,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)
@@ -757,8 +885,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);
@@ -781,30 +909,24 @@
                 continue;
             }
 
-            float positionInHotseat;
-            if (isAllAppsButton) {
-                // Note that there is no All Apps button in the hotseat,
-                // this position is only used as its convenient for animation purposes.
-                positionInHotseat = Utilities.isRtl(child.getResources())
-                        ? taskbarDp.numShownHotseatIcons
-                        : -1;
-            }  else if (isTaskbarDividerView) {
-                // Note that there is no taskbar divider view in the hotseat,
-                // this position is only used as its convenient for animation purposes.
-                positionInHotseat = Utilities.isRtl(child.getResources())
-                        ? taskbarDp.numShownHotseatIcons - 0.5f
-                        : -0.5f;
-            } else if (child.getTag() instanceof ItemInfo) {
-                positionInHotseat = ((ItemInfo) child.getTag()).screenId;
-            } else {
-                Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child);
-                continue;
+            int recentTaskIndex = -1;
+            if (isRecentTask) {
+                if (firstRecentTaskIndex < 0) {
+                    firstRecentTaskIndex = i;
+                }
+                recentTaskIndex = i - firstRecentTaskIndex;
             }
+            float positionInHotseat = getPositionInHotseat(taskbarDp.numShownHotseatIcons, child,
+                    mIsRtl, isAllAppsButton, isTaskbarDividerView,
+                    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;
@@ -813,6 +935,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();
@@ -835,6 +958,58 @@
         return controller;
     }
 
+    /**
+     * Returns the index of the given child relative to its position in hotseat.
+     * Examples:
+     * -1 is the item before the first hotseat item.
+     * -0.5 is between those (e.g. for the divider).
+     * {@link #ERROR_POSITION_IN_HOTSEAT_NOT_FOUND} if there's no calculation relative to hotseat.
+     */
+    @VisibleForTesting
+    float getPositionInHotseat(int numShownHotseatIcons, View child, boolean isRtl,
+            boolean isAllAppsButton, boolean isTaskbarDividerView, boolean isDividerForRecents,
+            int recentTaskIndex) {
+        float positionInHotseat;
+        // Note that there is no All Apps button in the hotseat,
+        // this position is only used as it's convenient for animation purposes.
+        float allAppsButtonPositionInHotseat = isRtl
+                // Right after all hotseat items.
+                // [HHHHHH]|[>A<]
+                ? numShownHotseatIcons
+                // Right before all hotseat items.
+                // [>A<]|[HHHHHH]
+                : -1;
+        // Note that there are no recent tasks in the hotseat,
+        // this position is only used as it's convenient for animation purposes.
+        float firstRecentTaskPositionInHotseat = isRtl
+                // After all hotseat icons and All Apps button.
+                // [HHHHHH][A]|[>R<R]
+                ? numShownHotseatIcons + 1
+                // Right after all hotseat items.
+                // [A][HHHHHH]|[>R<R]
+                : numShownHotseatIcons;
+        if (isAllAppsButton) {
+            positionInHotseat = allAppsButtonPositionInHotseat;
+        }  else if (isTaskbarDividerView) {
+            // Note that there is no taskbar divider view in the hotseat,
+            // this position is only used as it's convenient for animation purposes.
+            float relativePosition = isDividerForRecents
+                    ? firstRecentTaskPositionInHotseat
+                    : allAppsButtonPositionInHotseat;
+            positionInHotseat = relativePosition > 0
+                    ? relativePosition - DIVIDER_VIEW_POSITION_OFFSET
+                    : relativePosition + DIVIDER_VIEW_POSITION_OFFSET;
+        } else if (child.getTag() instanceof ItemInfo) {
+            positionInHotseat = ((ItemInfo) child.getTag()).screenId;
+        } else if (recentTaskIndex >= 0) {
+            positionInHotseat = firstRecentTaskPositionInHotseat + recentTaskIndex;
+        } else {
+            Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child);
+            return ERROR_POSITION_IN_HOTSEAT_NOT_FOUND;
+        }
+        return positionInHotseat;
+    }
+
     private boolean bubbleBarHasBubbles() {
         return mControllers.bubbleControllers.isPresent()
                 && mControllers.bubbleControllers.get().bubbleBarViewController.hasBubbles();
@@ -901,6 +1076,8 @@
                 if (groupTask.containsTask(task.key.id)) {
                     mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask);
                 }
+            } else if (view instanceof TaskbarOverflowView overflowButton) {
+                overflowButton.updateTaskIsShown(task);
             }
         }
     }
@@ -919,9 +1096,22 @@
                 "ALPHA_INDEX_RECENTS_DISABLED",
                 "ALPHA_INDEX_NOTIFICATION_EXPANDED",
                 "ALPHA_INDEX_ASSISTANT_INVOKED",
-                "ALPHA_INDEX_IME_BUTTON_NAV",
                 "ALPHA_INDEX_SMALL_SCREEN");
 
         mModelCallbacks.dumpLogs(prefix + "\t", pw);
     }
+
+    /** Enables model loading for tests. */
+    @VisibleForTesting
+    public static void enableModelLoadingForTests(boolean enable) {
+        sEnableModelLoadingForTests = enable;
+    }
+
+    private ObjectAnimator createTaskbarIconsShiftAnimator(float translationX) {
+        ObjectAnimator animator = mIconsTranslationXForNavbar.animateToValue(translationX);
+        animator.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS);
+        animator.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
+        animator.setInterpolator(Interpolators.EMPHASIZED);
+        return animator;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
index 5a5ff8e..c380c8d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
@@ -41,7 +41,8 @@
 class VoiceInteractionWindowController(val context: TaskbarActivityContext) :
     TaskbarControllers.LoggableTaskbarController, TaskbarControllers.BackgroundRendererController {
 
-    private val isSeparateBackgroundEnabled = !DisplayController.isTransientTaskbar(context)
+    private val isSeparateBackgroundEnabled =
+        !DisplayController.isTransientTaskbar(context) && !context.isPhoneMode
     private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context)
     private val nonTouchableInsetsComputer =
         ViewTreeObserver.OnComputeInternalInsetsListener {
@@ -109,7 +110,7 @@
     }
 
     fun setIsVoiceInteractionWindowVisible(visible: Boolean, skipAnim: Boolean) {
-        if (isVoiceInteractionWindowVisible == visible) {
+        if (isVoiceInteractionWindowVisible == visible || context.isPhoneMode) {
             return
         }
         isVoiceInteractionWindowVisible = visible
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java
index 5d91acd..a46845a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java
@@ -17,13 +17,10 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.R;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 
 import java.util.Optional;
@@ -47,23 +44,6 @@
     }
 
     @Override
-    protected View inflateSearchBar() {
-        if (isSearchSupported()) {
-            return super.inflateSearchBar();
-        }
-
-        // Remove top padding of header, since we do not have any search
-        mHeader.setPadding(mHeader.getPaddingLeft(), 0,
-                mHeader.getPaddingRight(), mHeader.getPaddingBottom());
-
-        TaskbarAllAppsFallbackSearchContainer searchView =
-                new TaskbarAllAppsFallbackSearchContainer(getContext(), null);
-        searchView.setId(R.id.search_container_all_apps);
-        searchView.setVisibility(GONE);
-        return searchView;
-    }
-
-    @Override
     public void invalidateHeader() {
         super.invalidateHeader();
         Optional.ofNullable(mOnInvalidateHeaderListener).ifPresent(
@@ -71,11 +51,6 @@
     }
 
     @Override
-    protected boolean isSearchSupported() {
-        return FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get();
-    }
-
-    @Override
     public boolean isInAllApps() {
         // All apps is always open
         return true;
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java
deleted file mode 100644
index 53fe06d..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java
+++ /dev/null
@@ -1,54 +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.taskbar.allapps;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.ExtendedEditText;
-import com.android.launcher3.allapps.ActivityAllAppsContainerView;
-import com.android.launcher3.allapps.SearchUiManager;
-
-/** Empty search container for Taskbar All Apps used as a fallback if search is not supported. */
-public class TaskbarAllAppsFallbackSearchContainer extends View implements SearchUiManager {
-    public TaskbarAllAppsFallbackSearchContainer(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public TaskbarAllAppsFallbackSearchContainer(
-            Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    @Override
-    public void initializeSearch(ActivityAllAppsContainerView<?> containerView) {
-        // Do nothing.
-    }
-
-    @Override
-    public void resetSearch() {
-        // Do nothing.
-    }
-
-    @Nullable
-    @Override
-    public ExtendedEditText getEditText() {
-        return null;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 90ac872..5830095 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.taskbar.allapps;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
-import static com.android.launcher3.Flags.enablePredictiveBackGesture;
 import static com.android.launcher3.touch.AllAppsSwipeController.ALL_APPS_FADE_MANUAL;
 import static com.android.launcher3.touch.AllAppsSwipeController.SCRIM_FADE_MANUAL;
 
@@ -40,7 +39,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 import com.android.launcher3.util.Themes;
@@ -181,9 +179,7 @@
         mContent = mAppsView;
 
         // Setup header protection for search bar, if enabled.
-        if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
-            mAppsView.setOnInvalidateHeaderListener(this::invalidate);
-        }
+        mAppsView.setOnInvalidateHeaderListener(this::invalidate);
 
         DeviceProfile dp = mActivityContext.getDeviceProfile();
         setShiftRange(dp.allAppsShiftRange);
@@ -193,14 +189,12 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mActivityContext.addOnDeviceProfileChangeListener(this);
-        if (enablePredictiveBackGesture()) {
-            mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(mViewOutlineProvider);
-            mAppsView.getAppsRecyclerViewContainer().setClipToOutline(true);
-            OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
-            if (dispatcher != null) {
-                dispatcher.registerOnBackInvokedCallback(
-                        OnBackInvokedDispatcher.PRIORITY_DEFAULT, this);
-            }
+        mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(mViewOutlineProvider);
+        mAppsView.getAppsRecyclerViewContainer().setClipToOutline(true);
+        OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
+        if (dispatcher != null) {
+            dispatcher.registerOnBackInvokedCallback(
+                    OnBackInvokedDispatcher.PRIORITY_DEFAULT, this);
         }
     }
 
@@ -208,13 +202,11 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mActivityContext.removeOnDeviceProfileChangeListener(this);
-        if (enablePredictiveBackGesture()) {
-            mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(null);
-            mAppsView.getAppsRecyclerViewContainer().setClipToOutline(false);
-            OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
-            if (dispatcher != null) {
-                dispatcher.unregisterOnBackInvokedCallback(this);
-            }
+        mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(null);
+        mAppsView.getAppsRecyclerViewContainer().setClipToOutline(false);
+        OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
+        if (dispatcher != null) {
+            dispatcher.unregisterOnBackInvokedCallback(this);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
index 4d0b376..6b72ab6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
@@ -21,7 +21,6 @@
 import com.android.launcher3.R
 import com.android.launcher3.allapps.AllAppsTransitionListener
 import com.android.launcher3.anim.PendingAnimation
-import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.dragndrop.DragOptions.PreDragCondition
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.util.ResourceBasedOverride
@@ -61,16 +60,11 @@
 
     companion object {
         @JvmStatic
-        fun newInstance(context: Context): TaskbarSearchSessionController {
-            if (!FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
-                return TaskbarSearchSessionController()
-            }
-
-            return Overrides.getObject(
+        fun newInstance(context: Context): TaskbarSearchSessionController =
+            Overrides.getObject(
                 TaskbarSearchSessionController::class.java,
                 context,
                 R.string.taskbar_search_session_controller_class,
             )
-        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 25939e1..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
@@ -71,6 +72,27 @@
             }
         }
 
+    /**
+     * Scale of the background in the x direction. Pivot is at the left edge if [anchorLeft] is
+     * `true` and at the right edge if it is `false`
+     */
+    var scaleX: Float = 1f
+        set(value) {
+            if (field != value) {
+                field = value
+                invalidateSelf()
+            }
+        }
+
+    /** Scale of the background in the y direction. Pivot is at the bottom edge. */
+    var scaleY: Float = 1f
+        set(value) {
+            if (field != value) {
+                field = value
+                invalidateSelf()
+            }
+        }
+
     init {
         val res = context.resources
         // configure fill paint
@@ -84,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)
@@ -111,25 +133,28 @@
     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()
-        val topOffset = backgroundHeight - bounds.height().toFloat()
+        val scaledBackgroundHeight = backgroundHeight * scaleY
+        val scaledWidth = width * scaleX
+        val topOffset = scaledBackgroundHeight - bounds.height().toFloat()
         val radius = backgroundHeight / 2f
-        val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - width)
-        val right = bounds.left + (if (anchorLeft) width else bounds.width().toFloat())
-        val top = bounds.top - topOffset + arrowVisibleHeight
 
-        val bottom = bounds.top + bounds.height().toFloat()
+        val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - scaledWidth)
+        val right = bounds.left + (if (anchorLeft) scaledWidth else bounds.width().toFloat())
+        // Calculate top with scaled heights for background and arrow to align with stash handle
+        val top = bounds.bottom - scaledBackgroundHeight + getScaledArrowVisibleHeight()
+        val bottom = bounds.bottom.toFloat()
+
         backgroundPath.addRoundRect(left, top, right, bottom, radius, radius, Path.Direction.CW)
         addArrowPathIfNeeded(backgroundPath, topOffset)
 
@@ -142,19 +167,20 @@
     private fun addArrowPathIfNeeded(sourcePath: Path, topOffset: Float) {
         if (!showingArrow || arrowHeightFraction <= 0) return
         val arrowPath = Path()
+        val scaledHeight = getScaledArrowHeight()
         RoundedArrowDrawable.addDownPointingRoundedTriangleToPath(
             arrowWidth,
-            arrowHeight,
+            scaledHeight,
             arrowTipRadius,
-            arrowPath
+            arrowPath,
         )
         // flip it horizontally
         val pathTransform = Matrix()
-        pathTransform.setRotate(180f, arrowWidth * 0.5f, arrowHeight * 0.5f)
+        pathTransform.setRotate(180f, arrowWidth * 0.5f, scaledHeight * 0.5f)
         arrowPath.transform(pathTransform)
         // shift to arrow position
         val arrowStart = bounds.left + arrowPositionX - (arrowWidth / 2f)
-        val arrowTop = (1 - arrowHeightFraction) * arrowVisibleHeight - topOffset
+        val arrowTop = (1 - arrowHeightFraction) * getScaledArrowVisibleHeight() - topOffset
         arrowPath.offset(arrowStart, arrowTop)
         // union with rectangle
         sourcePath.op(arrowPath, Path.Op.UNION)
@@ -170,6 +196,7 @@
 
     override fun setAlpha(alpha: Int) {
         fillPaint.alpha = alpha
+        strokePaint.alpha = mapToRange(alpha, 0, 255, 0, strokeAlpha, Interpolators.LINEAR)
         invalidateSelf()
     }
 
@@ -183,6 +210,7 @@
 
     fun setBackgroundHeight(newHeight: Float) {
         backgroundHeight = newHeight
+        invalidateSelf()
     }
 
     /**
@@ -199,10 +227,18 @@
         invalidateSelf()
     }
 
+    private fun getScaledArrowHeight(): Float {
+        return arrowHeight * scaleY
+    }
+
+    private fun getScaledArrowVisibleHeight(): Float {
+        return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight))
+    }
+
     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/BubbleBarBubbleIconsFactory.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubbleIconsFactory.kt
new file mode 100644
index 0000000..4330c5b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubbleIconsFactory.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
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import com.android.launcher3.icons.BaseIconFactory
+
+/** Bubble icons factory for the bubble bar. */
+class BubbleBarBubbleIconsFactory(context: Context, bubbleSize: Int) :
+    BaseIconFactory(context, context.resources.configuration.densityDpi, bubbleSize) {
+
+    /** Creates shadowed icon for the bubble bar. */
+    fun createShadowedIconBitmap(
+        icon: Drawable,
+        scale: Float,
+    ): Bitmap = super.createIconBitmap(icon, scale, MODE_WITH_SHADOW)
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index a3832cd..e0814d3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -15,13 +15,8 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 
-import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -33,50 +28,30 @@
 
 import android.annotation.BinderThread;
 import android.annotation.Nullable;
-import android.app.Notification;
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.content.pm.ShortcutInfo;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Matrix;
-import android.graphics.Path;
 import android.graphics.Point;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
 import android.os.Bundle;
 import android.os.SystemProperties;
-import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.util.PathParser;
-import android.view.LayoutInflater;
 
-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.TaskbarSharedState;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.bubbles.IBubblesListener;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
-import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
-import com.android.wm.shell.common.bubbles.BubbleInfo;
-import com.android.wm.shell.common.bubbles.RemovedBubble;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
+import com.android.wm.shell.shared.bubbles.BubbleInfo;
+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;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
@@ -135,18 +110,18 @@
 
     private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor(
             new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND));
-    private final LauncherApps mLauncherApps;
-    private final BubbleIconFactory mIconFactory;
     private final SystemUiProxy mSystemUiProxy;
 
     private BubbleBarItem mSelectedBubble;
-    private BubbleBarOverflow mOverflowBubble;
 
+    private TaskbarSharedState mSharedState;
     private ImeVisibilityChecker mImeVisibilityChecker;
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleStashController mBubbleStashController;
-    private BubbleStashedHandleViewController mBubbleStashedHandleViewController;
+    private Optional<BubbleStashedHandleViewController> mBubbleStashedHandleViewController;
     private BubblePinController mBubblePinController;
+    private BubbleCreator mBubbleCreator;
+    private BubbleBarLocationListener mBubbleBarLocationListener;
 
     // Cache last sent top coordinate to avoid sending duplicate updates to shell
     private int mLastSentBubbleBarTop;
@@ -167,6 +142,8 @@
         List<RemovedBubble> removedBubbles;
         List<String> bubbleKeysInOrder;
         Point expandedViewDropTargetSize;
+        boolean showOverflow;
+        boolean showOverflowChanged;
 
         // These need to be loaded in the background
         BubbleBarBubble addedBubble;
@@ -185,6 +162,8 @@
             removedBubbles = update.removedBubbles;
             bubbleKeysInOrder = update.bubbleKeysInOrder;
             expandedViewDropTargetSize = update.expandedViewDropTargetSize;
+            showOverflow = update.showOverflow;
+            showOverflowChanged = update.showOverflowChanged;
         }
     }
 
@@ -193,65 +172,56 @@
         mBarView = bubbleView; // Need the view for inflating bubble views.
 
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
-
-        if (sBubbleBarEnabled) {
-            mSystemUiProxy.setBubblesListener(this);
-        }
-        mLauncherApps = context.getSystemService(LauncherApps.class);
-        mIconFactory = new BubbleIconFactory(context,
-                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
-                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
-                context.getResources().getColor(R.color.important_conversation),
-                context.getResources().getDimensionPixelSize(
-                        com.android.internal.R.dimen.importance_ring_stroke_width));
     }
 
     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,
-            ImeVisibilityChecker imeVisibilityChecker) {
+            BubbleBarLocationListener bubbleBarLocationListener,
+            ImeVisibilityChecker imeVisibilityChecker,
+            TaskbarSharedState sharedState) {
+        mSharedState = sharedState;
         mImeVisibilityChecker = imeVisibilityChecker;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
         mBubblePinController = bubbleControllers.bubblePinController;
+        mBubbleCreator = bubbleControllers.bubbleCreator;
+        mBubbleBarLocationListener = bubbleBarLocationListener;
 
         bubbleControllers.runAfterInit(() -> {
+            restoreSavedState(sharedState);
             mBubbleBarViewController.setHiddenForBubbles(
                     !sBubbleBarEnabled || mBubbles.isEmpty());
-            mBubbleStashedHandleViewController.setHiddenForBubbles(
-                    !sBubbleBarEnabled || mBubbles.isEmpty());
+            mBubbleStashedHandleViewController.ifPresent(
+                    controller -> controller.setHiddenForBubbles(
+                            !sBubbleBarEnabled || mBubbles.isEmpty()));
             mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
                     key -> setSelectedBubbleInternal(mBubbles.get(key)));
             mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged);
+            mBubbleBarLocationListener.onBubbleBarLocationUpdated(
+                    mBubbleBarViewController.getBubbleBarLocation());
+            if (sBubbleBarEnabled) {
+                mSystemUiProxy.setBubblesListener(this);
+            }
         });
     }
 
     /**
-     * Creates and adds the overflow bubble to the bubble bar if it hasn't been created yet.
-     *
-     * <p>This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid inflating
-     * the overflow multiple times.
-     */
-    private void createAndAddOverflowIfNeeded() {
-        if (mOverflowBubble == null) {
-            BubbleBarOverflow overflow = createOverflow(mContext);
-            MAIN_EXECUTOR.execute(() -> {
-                // we're on the main executor now, so check that the overflow hasn't been created
-                // again to avoid races.
-                if (mOverflowBubble == null) {
-                    mBubbleBarViewController.addBubble(
-                            overflow, /* isExpanding= */ false, /* suppressAnimation= */ true);
-                    mOverflowBubble = overflow;
-                }
-            });
-        }
-    }
-
-    /**
      * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags.
      */
     public void updateStateForSysuiFlags(@SystemUiStateFlags long flags) {
@@ -259,10 +229,11 @@
         mBubbleBarViewController.setHiddenForSysui(hideBubbleBar);
 
         boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0;
-        mBubbleStashedHandleViewController.setHiddenForSysui(hideHandleView);
+        mBubbleStashedHandleViewController.ifPresent(
+                controller -> controller.setHiddenForSysui(hideHandleView));
 
         boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0;
-        mBubbleStashController.onSysuiLockedStateChange(sysuiLocked);
+        mBubbleStashController.setSysuiLocked(sysuiLocked);
     }
 
     //
@@ -280,23 +251,25 @@
                 || !update.currentBubbleList.isEmpty()) {
             // We have bubbles to load
             BUBBLE_STATE_EXECUTOR.execute(() -> {
-                createAndAddOverflowIfNeeded();
                 if (update.addedBubble != null) {
-                    viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
+                    viewUpdate.addedBubble = mBubbleCreator.populateBubble(mContext,
+                            update.addedBubble,
+                            mBarView,
                             null /* existingBubble */);
                 }
                 if (update.updatedBubble != null) {
                     BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
                     viewUpdate.updatedBubble =
-                            populateBubble(mContext, update.updatedBubble, mBarView,
+                            mBubbleCreator.populateBubble(mContext, update.updatedBubble,
+                                    mBarView,
                                     existingBubble);
                 }
                 if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
                     List<BubbleBarBubble> currentBubbles = new ArrayList<>();
                     for (int i = 0; i < update.currentBubbleList.size(); i++) {
-                        BubbleBarBubble b =
-                                populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
-                                        null /* existingBubble */);
+                        BubbleBarBubble b = mBubbleCreator.populateBubble(mContext,
+                                update.currentBubbleList.get(i), mBarView,
+                                null /* existingBubble */);
                         currentBubbles.add(b);
                     }
                     viewUpdate.currentBubbles = currentBubbles;
@@ -310,6 +283,24 @@
         }
     }
 
+    private void restoreSavedState(TaskbarSharedState sharedState) {
+        if (sharedState.bubbleBarLocation != null) {
+            updateBubbleBarLocationInternal(sharedState.bubbleBarLocation);
+        }
+        List<BubbleInfo> bubbleInfos = sharedState.bubbleInfoItems;
+        if (bubbleInfos == null || bubbleInfos.isEmpty()) return;
+        // Iterate in reverse because new bubbles are added in front and the list is in order.
+        for (int i = bubbleInfos.size() - 1; i >= 0; i--) {
+            BubbleBarBubble bubble = mBubbleCreator.populateBubble(mContext,
+                    bubbleInfos.get(i), mBarView, /* existingBubble = */ null);
+            if (bubble == null) {
+                Log.e(TAG, "Could not instantiate BubbleBarBubble for " + bubbleInfos.get(i));
+                continue;
+            }
+            addBubbleInternally(bubble, /* isExpanding= */ false, /* suppressAnimation= */ true);
+        }
+    }
+
     private void applyViewChanges(BubbleBarViewUpdate update) {
         final boolean isCollapsed = (update.expandedChanged && !update.expanded)
                 || (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
@@ -321,27 +312,46 @@
                 update.initialState || mBubbleBarViewController.isHiddenForSysui()
                         || mImeVisibilityChecker.isImeVisible();
 
+        if (update.initialState && mSharedState.hasSavedBubbles()) {
+            // clear restored state
+            mBubbleBarViewController.removeAllBubbles();
+            mBubbles.clear();
+        }
+
         BubbleBarBubble bubbleToSelect = null;
 
-        if (update.addedBubble != null && update.removedBubbles.size() == 1) {
+        if (Flags.enableOptionalBubbleOverflow()
+                && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null
+                && update.removedBubbles.isEmpty()) {
+            // A bubble was added from the overflow (& now it's empty / not showing)
+            mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
+        } else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
             // we're adding and removing a bubble at the same time. handle this as a single update.
             RemovedBubble removedBubble = update.removedBubbles.get(0);
             BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey());
             mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+            boolean showOverflow = update.showOverflowChanged && update.showOverflow;
             if (bubbleToRemove != null) {
                 mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble,
-                        bubbleToRemove, isExpanding, suppressAnimation);
+                        bubbleToRemove, isExpanding, suppressAnimation, showOverflow);
             } else {
                 mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
                         suppressAnimation);
                 Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
             }
         } else {
+            boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow()
+                    && update.showOverflowChanged && update.showOverflow;
             if (!update.removedBubbles.isEmpty()) {
                 for (int i = 0; i < update.removedBubbles.size(); i++) {
                     RemovedBubble removedBubble = update.removedBubbles.get(i);
                     BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
-                    if (bubble != null) {
+                    if (bubble != null && overflowNeedsToBeAdded) {
+                        // First removal, show the overflow
+                        overflowNeedsToBeAdded = false;
+                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
+                    } else if (bubble != null) {
                         mBubbleBarViewController.removeBubble(bubble);
                     } else {
                         Log.w(TAG, "trying to remove bubble that doesn't exist: "
@@ -354,6 +364,18 @@
                 mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
                         suppressAnimation);
             }
+            if (Flags.enableOptionalBubbleOverflow()
+                    && update.showOverflowChanged
+                    && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) {
+                mBubbleBarViewController.showOverflow(update.showOverflow);
+            }
+        }
+
+        // if a bubble was updated upstream, but removed before the update was received, add it back
+        if (update.updatedBubble != null && !mBubbles.containsKey(update.updatedBubble.getKey())) {
+            mBubbles.put(update.updatedBubble.getKey(), update.updatedBubble);
+            mBubbleBarViewController.addBubble(
+                    update.updatedBubble, isExpanding, suppressAnimation);
         }
 
         if (update.addedBubble != null && isCollapsed) {
@@ -366,8 +388,7 @@
             for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
                 BubbleBarBubble bubble = update.currentBubbles.get(i);
                 if (bubble != null) {
-                    mBubbles.put(bubble.getKey(), bubble);
-                    mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
+                    addBubbleInternally(bubble, isExpanding, suppressAnimation);
                     if (isCollapsed) {
                         // If we're collapsed, the most recently added bubble will be selected.
                         bubbleToSelect = bubble;
@@ -378,10 +399,14 @@
                 }
             }
         }
+        if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) {
+            mBubbleBarViewController.showOverflow(true);
+        }
 
         // Adds and removals have happened, update visibility before any other visual changes.
         mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
-        mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
+        mBubbleStashedHandleViewController.ifPresent(
+                controller -> controller.setHiddenForBubbles(mBubbles.isEmpty()));
 
         if (mBubbles.isEmpty()) {
             // all bubbles were removed. clear the selected bubble
@@ -392,10 +417,13 @@
             // Updates mean the dot state may have changed; any other changes were updated in
             // the populateBubble step.
             BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
-            // If we're not stashed, we're visible so animate
-            bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
-            mBubbleBarViewController.animateBubbleNotification(
-                    bb, /* isExpanding= */ false, /* isUpdate= */ true);
+            if (suppressAnimation) {
+                // since we're not animating this update, we should update the dot visibility here.
+                bb.getView().updateDotVisibility(/* animate= */ false);
+            } else {
+                mBubbleBarViewController.animateBubbleNotification(
+                        bb, /* isExpanding= */ false, /* isUpdate= */ true);
+            }
         }
         if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
             // Create the new list
@@ -437,6 +465,7 @@
             }
         }
         if (update.bubbleBarLocation != null) {
+            mSharedState.bubbleBarLocation = update.bubbleBarLocation;
             if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) {
                 updateBubbleBarLocationInternal(update.bubbleBarLocation);
             }
@@ -456,15 +485,6 @@
     /** Tells WMShell to show the currently selected bubble. */
     public void showSelectedBubble() {
         if (getSelectedBubbleKey() != null) {
-            if (mSelectedBubble instanceof BubbleBarBubble) {
-                // Because we've visited this bubble, we should suppress the notification.
-                // This is updated on WMShell side when we show the bubble, but that update isn't
-                // passed to launcher, instead we apply it directly here.
-                BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
-                info.setFlags(
-                        info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
-                mSelectedBubble.getView().updateDotVisibility(true /* animate */);
-            }
             mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen();
             mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop);
         } else {
@@ -508,153 +528,36 @@
      * <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) {
         mBubbleBarViewController.setBubbleBarLocation(location);
         mBubbleStashController.setBubbleBarLocation(location);
+        mBubbleBarLocationListener.onBubbleBarLocationUpdated(location);
     }
 
     @Override
     public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
         MAIN_EXECUTOR.execute(
-                () -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
+                () -> {
+                    mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation);
+                    mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation);
+                });
+    }
+
+    /** Notifies WMShell to show the expanded view. */
+    void showExpandedView() {
+        mSystemUiProxy.showExpandedView();
     }
 
     //
     // Loading data for the bubbles
     //
 
-    @Nullable
-    private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
-            @Nullable BubbleBarBubble existingBubble) {
-        String appName;
-        Bitmap badgeBitmap;
-        Bitmap bubbleBitmap;
-        Path dotPath;
-        int dotColor;
-
-        boolean isImportantConvo = b.isImportantConversation();
-
-        ShortcutRequest.QueryResult result = new ShortcutRequest(context,
-                new UserHandle(b.getUserId()))
-                .forPackage(b.getPackageName(), b.getShortcutId())
-                .query(FLAG_MATCH_DYNAMIC
-                        | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
-                        | FLAG_MATCH_CACHED
-                        | FLAG_GET_PERSONS_DATA);
-
-        ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
-        if (shortcutInfo == null) {
-            Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey()
-                    + " with shortcutId: " + b.getShortcutId());
-        }
-
-        ApplicationInfo appInfo;
-        try {
-            appInfo = mLauncherApps.getApplicationInfo(
-                    b.getPackageName(),
-                    0,
-                    new UserHandle(b.getUserId()));
-        } catch (PackageManager.NameNotFoundException e) {
-            // If we can't find package... don't think we should show the bubble.
-            Log.w(TAG, "Unable to find packageName: " + b.getPackageName());
-            return null;
-        }
-        if (appInfo == null) {
-            Log.w(TAG, "Unable to find appInfo: " + b.getPackageName());
-            return null;
-        }
-        PackageManager pm = context.getPackageManager();
-        appName = String.valueOf(appInfo.loadLabel(pm));
-        Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
-        Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId()));
-
-        // Badged bubble image
-        Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
-                b.getIcon());
-        if (bubbleDrawable == null) {
-            // Default to app icon
-            bubbleDrawable = appIcon;
-        }
-
-        BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
-        badgeBitmap = badgeBitmapInfo.icon;
-
-        float[] bubbleBitmapScale = new float[1];
-        bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
-
-        // Dot color & placement
-        Path iconPath = PathParser.createPathFromPathData(
-                context.getResources().getString(
-                        com.android.internal.R.string.config_icon_mask));
-        Matrix matrix = new Matrix();
-        float scale = bubbleBitmapScale[0];
-        float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
-        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
-                radius /* pivot y */);
-        iconPath.transform(matrix);
-        dotPath = iconPath;
-        dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
-                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
-
-        if (existingBubble == null) {
-            LayoutInflater inflater = LayoutInflater.from(context);
-            BubbleView bubbleView = (BubbleView) inflater.inflate(
-                    R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
-
-            BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
-                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
-            bubbleView.setBubble(bubble);
-            return bubble;
-        } else {
-            // If we already have a bubble (so it already has an inflated view), update it.
-            existingBubble.setInfo(b);
-            existingBubble.setBadge(badgeBitmap);
-            existingBubble.setIcon(bubbleBitmap);
-            existingBubble.setDotColor(dotColor);
-            existingBubble.setDotPath(dotPath);
-            existingBubble.setAppName(appName);
-            return existingBubble;
-        }
-    }
-
-    private BubbleBarOverflow createOverflow(Context context) {
-        Bitmap bitmap = createOverflowBitmap(context);
-        LayoutInflater inflater = LayoutInflater.from(context);
-        BubbleView bubbleView = (BubbleView) inflater.inflate(
-                R.layout.bubble_bar_overflow_button, mBarView, false /* attachToRoot */);
-        BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
-        bubbleView.setOverflow(overflow, bitmap);
-        return overflow;
-    }
-
-    private Bitmap createOverflowBitmap(Context context) {
-        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
-                R.drawable.bubble_ic_overflow_button);
-
-        final TypedArray ta = mContext.obtainStyledAttributes(
-                new int[]{
-                        com.android.internal.R.attr.materialColorOnPrimaryFixed,
-                        com.android.internal.R.attr.materialColorPrimaryFixed
-                });
-        int overflowIconColor = ta.getColor(0, Color.WHITE);
-        int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
-        ta.recycle();
-
-        iconDrawable.setTint(overflowIconColor);
-
-        int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
-        Drawable foreground = new InsetDrawable(iconDrawable, inset);
-        Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
-                foreground);
-
-        return mIconFactory.createBadgedIconBitmap(drawable).icon;
-    }
-
     private void onBubbleBarBoundsChanged() {
         int newTop = mBarView.getRestingTopPositionOnScreen();
         if (newTop != mLastSentBubbleBarTop) {
@@ -663,9 +566,25 @@
         }
     }
 
+    private void addBubbleInternally(BubbleBarBubble bubble, boolean isExpanding,
+            boolean suppressAnimation) {
+        mBubbles.put(bubble.getKey(), bubble);
+        mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
+    }
+
     /** Interface for checking whether the IME is visible. */
     public interface ImeVisibilityChecker {
         /** Whether the IME is visible. */
         boolean isImeVisible();
     }
+
+    /** Listener of {@link BubbleBarLocation} updates. */
+    public interface BubbleBarLocationListener {
+
+        /** Called when {@link BubbleBarLocation} is animated, but change is not yet final. */
+        void onBubbleBarLocationAnimated(BubbleBarLocation location);
+
+        /** Called when {@link BubbleBarLocation} is updated permanently. */
+        void onBubbleBarLocationUpdated(BubbleBarLocation location);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 39d1ed7..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.wm.shell.common.bubbles.BubbleInfo
+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/BubbleBarLocationCompositeListener.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.kt
new file mode 100644
index 0000000..8e176ac
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.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.launcher3.taskbar.bubbles
+
+import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+
+/** Composite implementation of [BubbleBarLocationListener] interface */
+class BubbleBarLocationCompositeListener(private val listeners: List<BubbleBarLocationListener>) :
+    BubbleBarLocationListener {
+
+    constructor(vararg listeners: BubbleBarLocationListener) : this(listOf(*listeners))
+
+    override fun onBubbleBarLocationAnimated(location: BubbleBarLocation?) {
+        listeners.forEach { it.onBubbleBarLocationAnimated(location) }
+    }
+
+    override fun onBubbleBarLocationUpdated(location: BubbleBarLocation?) {
+        listeners.forEach { it.onBubbleBarLocationUpdated(location) }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
index 9e5ffc9..a34fab2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
@@ -27,8 +27,10 @@
 import android.widget.FrameLayout
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.R
-import com.android.wm.shell.common.bubbles.BaseBubblePinController
-import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.wm.shell.shared.bubbles.BaseBubblePinController
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 
 /**
  * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar
@@ -41,12 +43,17 @@
 
     private lateinit var bubbleBarViewController: BubbleBarViewController
     private lateinit var bubbleStashController: BubbleStashController
+    private lateinit var bubbleBarLocationListener: BubbleBarLocationListener
     private var exclRectWidth: Float = 0f
     private var exclRectHeight: Float = 0f
 
     private var dropTargetView: View? = null
 
-    fun init(bubbleControllers: BubbleControllers) {
+    fun init(
+        bubbleControllers: BubbleControllers,
+        bubbleBarLocationListener: BubbleBarLocationListener
+    ) {
+        this.bubbleBarLocationListener = bubbleBarLocationListener
         bubbleBarViewController = bubbleControllers.bubbleBarViewController
         bubbleStashController = bubbleControllers.bubbleStashController
         exclRectWidth = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width)
@@ -85,6 +92,7 @@
 
         val bounds = bubbleBarViewController.bubbleBarBounds
         val horizontalMargin = bubbleBarViewController.horizontalMargin
+        bubbleBarLocationListener.onBubbleBarLocationAnimated(location)
         dropTargetView?.updateLayoutParams<FrameLayout.LayoutParams> {
             width = bounds.width()
             height = bounds.height()
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 fd989b1..350f56f 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;
@@ -30,6 +26,7 @@
 import android.content.Context;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.LayoutDirection;
@@ -38,15 +35,16 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+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.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -81,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;
-    private 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;
-
-    private 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.
@@ -122,8 +109,6 @@
 
     private final BubbleBarBackground mBubbleBarBackground;
 
-    private boolean mIsAnimatingNewBubble = false;
-
     /**
      * The current bounds of all the bubble bar. Note that these bounds may not account for
      * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which
@@ -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
@@ -187,6 +172,9 @@
     private BubbleView mDismissedByDragBubbleView;
     private float mAlphaDuringDrag = 1f;
 
+    /** Additional translation in the y direction that is applied to each bubble */
+    private float mBubbleOffsetY;
+
     private Controller mController;
 
     private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED;
@@ -205,7 +193,6 @@
 
     public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        setAlpha(0);
         setVisibility(INVISIBLE);
         mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
         mBubbleBarPadding = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
@@ -221,31 +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());
-                    }
-                    updateWidth();
-                },
-                /* onUpdate= */ animator -> {
-                    updateBubblesLayoutProperties(mBubbleBarLocation);
-                    invalidate();
-                });
     }
 
 
@@ -302,6 +264,57 @@
     }
 
     /**
+     * Set scale for bubble bar background in x direction
+     */
+    public void setBackgroundScaleX(float scaleX) {
+        mBubbleBarBackground.setScaleX(scaleX);
+    }
+
+    /**
+     * Set scale for bubble bar background in y direction
+     */
+    public void setBackgroundScaleY(float scaleY) {
+        mBubbleBarBackground.setScaleY(scaleY);
+    }
+
+    /**
+     * Set alpha for bubble views
+     */
+    public void setBubbleAlpha(float alpha) {
+        for (int i = 0; i < getChildCount(); i++) {
+            getChildAt(i).setAlpha(alpha);
+        }
+    }
+
+    /**
+     * Set alpha for bar background
+     */
+    public void setBackgroundAlpha(float alpha) {
+        mBubbleBarBackground.setAlpha((int) (255 * alpha));
+    }
+
+    /**
+     * Sets offset of each bubble view in the y direction from the base position in the bar.
+     */
+    public void setBubbleOffsetY(float offsetY) {
+        mBubbleOffsetY = offsetY;
+        for (int i = 0; i < getChildCount(); i++) {
+            getChildAt(i).setTranslationY(getBubbleTranslationY());
+        }
+    }
+
+    /**
+     * 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
@@ -318,7 +331,7 @@
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             View childView = getChildAt(i);
-            childView.setScaleY(mIconScale);
+            childView.setScaleX(mIconScale);
             childView.setScaleY(mIconScale);
             FrameLayout.LayoutParams params = (LayoutParams) childView.getLayoutParams();
             params.height = (int) mIconSize;
@@ -363,6 +376,49 @@
         }
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        // Always show only expand action as the menu is only for collapsed bubble bar
+        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+        info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all,
+                getResources().getString(R.string.bubble_bar_action_dismiss_all)));
+        if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+                    getResources().getString(R.string.bubble_bar_action_move_right)));
+        } else {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+                    getResources().getString(R.string.bubble_bar_action_move_left)));
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action,
+            @androidx.annotation.Nullable Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+            mController.expandBubbleBar();
+            return true;
+        }
+        if (action == R.id.action_dismiss_all) {
+            mController.dismissBubbleBar();
+            return true;
+        }
+        if (action == R.id.action_move_left) {
+            mController.updateBubbleBarLocation(BubbleBarLocation.LEFT,
+                    BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR);
+            return true;
+        }
+        if (action == R.id.action_move_right) {
+            mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT,
+                    BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR);
+            return true;
+        }
+        return false;
+    }
+
     @SuppressLint("RtlHardcoded")
     private void onBubbleBarLocationChanged() {
         final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
@@ -371,6 +427,7 @@
         LayoutParams lp = (LayoutParams) getLayoutParams();
         lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
         setLayoutParams(lp); // triggers a relayout
+        updateBubbleAccessibilityStates();
     }
 
     /**
@@ -490,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}.
@@ -601,7 +611,9 @@
         }
         setAlphaDuringBubbleDrag(1f);
         setTranslationX(0f);
-        setAlpha(1f);
+        if (mIsBarExpanded && getBubbleChildCount() > 0) {
+            setAlpha(1f);
+        }
     }
 
     /**
@@ -613,13 +625,25 @@
         return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY();
     }
 
-    /**
-     * Updates the bounds with translation that may have been applied and returns the result.
-     */
+    /** Returns the bounds with translation that may have been applied. */
     public Rect getBubbleBarBounds() {
-        mBubbleBarBounds.top = getTop() + (int) getTranslationY() + mPointerSize;
-        mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY();
-        return mBubbleBarBounds;
+        Rect bounds = new Rect(mBubbleBarBounds);
+        bounds.top = getTop() + (int) getTranslationY() + mPointerSize;
+        bounds.bottom = getBottom() + (int) getTranslationY();
+        return bounds;
+    }
+
+    /** Returns the expanded bounds with translation that may have been applied. */
+    public Rect getBubbleBarExpandedBounds() {
+        Rect expandedBounds = getBubbleBarBounds();
+        if (!isExpanded() || isExpanding()) {
+            if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+                expandedBounds.right = expandedBounds.left + (int) expandedWidth();
+            } else {
+                expandedBounds.left = expandedBounds.right - (int) expandedWidth();
+            }
+        }
+        return expandedBounds;
     }
 
     /**
@@ -654,25 +678,18 @@
         return mRelativePivotY;
     }
 
-    /** Notifies the bubble bar that a new bubble animation is starting. */
-    public void onAnimatingBubbleStarted() {
-        mIsAnimatingNewBubble = true;
-    }
-
-    /** Notifies the bubble bar that a new bubble animation is complete. */
-    public void onAnimatingBubbleCompleted() {
-        mIsAnimatingNewBubble = false;
-    }
-
     /** Add a new bubble to the bubble bar. */
-    public void addBubble(View bubble) {
+    public void addBubble(BubbleView bubble) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
+        final int index = bubble.isOverflow() ? getChildCount() : 0;
+
         if (isExpanded()) {
             // if we're expanded scale the new bubble in
             bubble.setScaleX(0f);
             bubble.setScaleY(0f);
-            addView(bubble, 0, lp);
+            addView(bubble, index, lp);
+            bubble.showDotIfNeeded(/* animate= */ false);
 
             mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
                     getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -680,7 +697,7 @@
 
                 @Override
                 public void onAnimationEnd() {
-                    updateWidth();
+                    updateLayoutParams();
                     mBubbleAnimator = null;
                 }
 
@@ -700,23 +717,37 @@
             };
             mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
         } else {
-            addView(bubble, 0, lp);
+            addView(bubble, index, lp);
         }
     }
 
     /** Add a new bubble and remove an old bubble from the bubble bar. */
-    public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) {
+    public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble,
+            Runnable onEndRunnable) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
+        boolean isOverflowSelected = mSelectedBubbleView.isOverflow();
+        boolean removingOverflow = removedBubble.isOverflow();
+        boolean addingOverflow = addedBubble.isOverflow();
+
         if (!isExpanded()) {
             removeView(removedBubble);
-            addView(addedBubble, 0, lp);
+            int index = addingOverflow ? getChildCount() : 0;
+            addView(addedBubble, index, lp);
+            if (onEndRunnable != null) {
+                onEndRunnable.run();
+            }
             return;
         }
+        int index = addingOverflow ? getChildCount() : 0;
         addedBubble.setScaleX(0f);
         addedBubble.setScaleY(0f);
-        addView(addedBubble, 0, lp);
+        addView(addedBubble, index, lp);
 
+        if (isOverflowSelected && removingOverflow) {
+            // The added bubble will be selected
+            mSelectedBubbleView = addedBubble;
+        }
         int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
         int indexOfBubbleToRemove = indexOfChild(removedBubble);
 
@@ -727,8 +758,11 @@
             @Override
             public void onAnimationEnd() {
                 removeView(removedBubble);
-                updateWidth();
+                updateLayoutParams();
                 mBubbleAnimator = null;
+                if (onEndRunnable != null) {
+                    onEndRunnable.run();
+                }
             }
 
             @Override
@@ -753,13 +787,13 @@
                 listener);
     }
 
-    // TODO: (b/280605790) animate it
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         super.addView(child, index, params);
-        updateWidth();
+        updateLayoutParams();
         updateBubbleAccessibilityStates();
         updateContentDescription();
+        updateDotsAndBadgesIfCollapsed();
     }
 
     /** Removes the given bubble from the bubble bar. */
@@ -821,16 +855,60 @@
             mSelectedBubbleView = null;
             mBubbleBarBackground.showArrow(false);
         }
-        updateWidth();
+        updateLayoutParams();
         updateBubbleAccessibilityStates();
         updateContentDescription();
         mDismissedByDragBubbleView = null;
+        updateDotsAndBadgesIfCollapsed();
     }
 
-    private void updateWidth() {
-        LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
-        lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth());
-        setLayoutParams(lp);
+    /**
+     * Return child views in the order which they are shown on the screen.
+     * <p>
+     * Child views (bubbles) are always ordered based on recency. The most recent bubble is at index
+     * 0.
+     * For example if the child views are (1)(2)(3) then (1) is the most recent bubble and at index
+     * 0.<br>
+     *
+     * How bubbles show up on the screen depends on the bubble bar location. If the bar is on the
+     * left, the most recent bubble is shown on the right. The bubbles from the example above would
+     * be shown as: (3)(2)(1).<br>
+     *
+     * If bubble bar is on the right, then the most recent bubble is on the left. Bubbles from the
+     * example above would be shown as: (1)(2)(3).
+     */
+    private List<View> getChildViewsInOnScreenOrder() {
+        List<View> childViews = new ArrayList<>(getChildCount());
+        for (int i = 0; i < getChildCount(); i++) {
+            childViews.add(getChildAt(i));
+        }
+        if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+            // Visually child views are shown in reverse order when bar is on the left
+            return childViews.reversed();
+        }
+        return childViews;
+    }
+
+    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 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();
+            }
+        }
     }
 
     private void updateLayoutParams() {
@@ -860,16 +938,12 @@
         final float currentWidth = getWidth();
         final float expandedWidth = expandedWidth();
         final float collapsedWidth = collapsedWidth();
-        int bubbleCount = getChildCount();
-        float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0);
-        float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight();
-        // When translating X & Y the scale is ignored, so need to deduct it from the translations
-        final float ty = bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift();
-        final boolean animate = getVisibility() == VISIBLE;
+        int childCount = getChildCount();
+        final float ty = getBubbleTranslationY();
         final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
         // elevation state is opposite to widthState - when expanded all icons are flat
         float elevationState = (1 - widthState);
-        for (int i = 0; i < bubbleCount; i++) {
+        for (int i = 0; i < childCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
                 // Skip the dragged bubble. Its translation is managed by the drag controller.
@@ -888,19 +962,22 @@
             bv.setTranslationY(ty);
 
             // the position of the bubble when the bar is fully expanded
-            final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
+            final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft);
             // the position of the bubble when the bar is fully collapsed
-            final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft);
+            final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft);
 
             // slowly animate elevation while keeping correct Z ordering
             float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
             bv.setZ(fullElevationForChild * elevationState);
 
-            // only update the dot scale if we're expanding or collapsing
-            // TODO b/351904597: update the dot for the first bubble after removal and reorder
-            // since those might happen when the bar is collapsed and will need their dot back
+            // only update the dot and badge scale if we're expanding or collapsing
             if (mWidthAnimator.isRunning()) {
-                bv.setDotScale(widthState);
+                // The dot for the selected bubble scales in the opposite direction of the expansion
+                // animation.
+                bv.showDotIfNeeded(bv == mSelectedBubbleView ? 1 - widthState : widthState);
+                // The badge for the selected bubble is always at full scale. All other bubbles
+                // scale according to the expand animation.
+                bv.setBadgeScale(bv == mSelectedBubbleView ? 1 : widthState);
             }
 
             if (mIsBarExpanded) {
@@ -909,26 +986,19 @@
                 // where the bubble will end up when the animation ends
                 final float targetX = expandedX + expandedBarShift;
                 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
-                // When we're expanded, the badge is visible for all bubbles
-                bv.updateBadgeVisibility(/* show= */ true);
-                bv.setAlpha(1);
+                bv.setVisibility(VISIBLE);
             } else {
                 // If bar is on the right, account for bubble bar expanding and shifting left
                 final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
                 final float targetX = collapsedX + collapsedBarShift;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
-                // The badge is always visible for the first bubble
-                bv.updateBadgeVisibility(/* show= */ i == 0);
-                // If we're fully collapsed, hide all bubbles except for the first 2. If there are
-                // only 2 bubbles, hide the second bubble as well because it's the overflow.
+                // If we're fully collapsed, hide all bubbles except for the first 2, excluding
+                // the overflow.
                 if (widthState == 0) {
-                    if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
-                        bv.setAlpha(0);
-                    } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1
-                            && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) {
-                        bv.setAlpha(0);
+                    if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
+                        bv.setVisibility(INVISIBLE);
                     } else {
-                        bv.setAlpha(1);
+                        bv.setVisibility(VISIBLE);
                     }
                 }
             }
@@ -981,22 +1051,36 @@
         return translationX - getScaleIconShift();
     }
 
-    private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) {
-        if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
+    private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) {
+        if (bubbleIndex < 0 || bubbleIndex >= childCount) {
             return 0;
         }
         float translationX;
         if (onLeft) {
-            // Shift the first bubble only if there are more bubbles in addition to overflow
-            translationX = mBubbleBarPadding + (
-                    bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
-                            ? mIconOverlapAmount : 0);
+            // Shift the first bubble only if there are more bubbles
+            if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
+                translationX = mIconOverlapAmount;
+            } else {
+                translationX = 0f;
+            }
         } else {
-            translationX = mBubbleBarPadding + (
-                    bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED
-                            ? 0 : mIconOverlapAmount);
+            // when the bar is on the right, the first bubble always has translation 0. the only
+            // case where another bubble has translation 0 is when we only have 1 bubble and the
+            // overflow. otherwise all other bubbles should be shifted by the overlap amount.
+            if (bubbleIndex == 0 || getBubbleChildCount() == 1) {
+                translationX = 0f;
+            } else {
+                translationX = mIconOverlapAmount;
+            }
         }
-        return translationX - getScaleIconShift();
+        return mBubbleBarPadding + translationX - getScaleIconShift();
+    }
+
+    private float getBubbleTranslationY() {
+        float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0);
+        float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight();
+        // When translating X & Y the scale is ignored, so need to deduct it from the translations
+        return mBubbleOffsetY + bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift();
     }
 
     /**
@@ -1025,6 +1109,7 @@
             }
             updateBubblesLayoutProperties(mBubbleBarLocation);
             updateContentDescription();
+            updateDotsAndBadgesIfCollapsed();
         }
     }
 
@@ -1049,6 +1134,14 @@
         if (mBubbleAnimator == null) {
             updateArrowForSelected(previouslySelectedBubble != null);
         }
+        if (view != null) {
+            if (isExpanded()) {
+                view.markSeen();
+            } else {
+                // when collapsed, the selected bubble should show the dot if it has it
+                view.showDotIfNeeded(/* animate= */ true);
+            }
+        }
     }
 
     /**
@@ -1150,12 +1243,10 @@
             mIsBarExpanded = isBarExpanded;
             updateArrowForSelected(/* shouldAnimate= */ false);
             setOrUnsetClickListener();
-            if (isBarExpanded) {
-                mWidthAnimator.start();
-            } else {
-                mWidthAnimator.reverse();
-            }
+            mWidthAnimator = createExpansionAnimator(isBarExpanded);
+            mWidthAnimator.start();
             updateBubbleAccessibilityStates();
+            announceExpandedStateChange();
         }
     }
 
@@ -1167,6 +1258,13 @@
     }
 
     /**
+     * Returns whether the bubble bar is expanding.
+     */
+    public boolean isExpanding() {
+        return mWidthAnimator.isRunning() && mIsBarExpanded;
+    }
+
+    /**
      * Get width of the bubble bar as if it would be expanded.
      *
      * @return width of the bubble bar in its expanded state, regardless of current width
@@ -1183,16 +1281,24 @@
         return totalIconSize + totalSpace + horizontalPadding;
     }
 
-    private float collapsedWidth() {
-        final int childCount = getChildCount();
+    /**
+     * Get width of the bubble bar if it is collapsed
+     */
+    float collapsedWidth() {
+        final int bubbleChildCount = getBubbleChildCount();
         final float horizontalPadding = 2 * mBubbleBarPadding;
-        // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
-        // Otherwise just the first bubble should be visible because we don't show the overflow.
-        return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED
+        // 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
                 : getScaledIconSize() + horizontalPadding;
     }
 
+    /** Returns the child count excluding the overflow if it's present. */
+    int getBubbleChildCount() {
+        return hasOverflow() ? getChildCount() - 1 : getChildCount();
+    }
+
     private float getBubbleBarExpandedHeight() {
         return getBubbleBarCollapsedHeight() + mPointerSize;
     }
@@ -1207,7 +1313,7 @@
      * touch bounds.
      */
     public boolean isEventOverAnyItem(MotionEvent ev) {
-        if (getVisibility() == View.VISIBLE) {
+        if (getVisibility() == VISIBLE) {
             getBoundsOnScreen(mTempRect);
             return mTempRect.contains((int) ev.getX(), (int) ev.getY());
         }
@@ -1216,9 +1322,7 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (mIsAnimatingNewBubble) {
-            mController.onBubbleBarTouchedWhileAnimating();
-        }
+        mController.onBubbleBarTouched();
         if (!mIsBarExpanded) {
             // When the bar is collapsed, all taps on it should expand it.
             return true;
@@ -1226,13 +1330,8 @@
         return super.onInterceptTouchEvent(ev);
     }
 
-    /** Whether a new bubble is currently animating. */
-    public boolean isAnimatingNewBubble() {
-        return mIsAnimatingNewBubble;
-    }
-
-    private boolean hasOverview() {
-        // Overview is always the last bubble
+    private boolean hasOverflow() {
+        // Overflow is always the last bubble
         View lastChild = getChildAt(getChildCount() - 1);
         if (lastChild instanceof BubbleView bubbleView) {
             return bubbleView.getBubble() instanceof BubbleBarOverflow;
@@ -1241,21 +1340,39 @@
     }
 
     private void updateBubbleAccessibilityStates() {
-        final int childA11y;
         if (mIsBarExpanded) {
             // Bar is expanded, focus on the bubbles
             setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-            childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+
+            // Set up a11y navigation order. Get list of child views in the order they are shown
+            // on screen. And use that to set up navigation so that swiping left focuses the view
+            // on the left and swiping right focuses view on the right.
+            View prevChild = null;
+            for (View childView : getChildViewsInOnScreenOrder()) {
+                childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+                childView.setFocusable(true);
+                final View finalPrevChild = prevChild;
+                // Always need to set a new delegate to clear out any previous.
+                childView.setAccessibilityDelegate(new AccessibilityDelegate() {
+                    @Override
+                    public void onInitializeAccessibilityNodeInfo(View host,
+                            AccessibilityNodeInfo info) {
+                        super.onInitializeAccessibilityNodeInfo(host, info);
+                        if (finalPrevChild != null) {
+                            info.setTraversalAfter(finalPrevChild);
+                        }
+                    }
+                });
+                prevChild = childView;
+            }
         } else {
             // Bar is collapsed, only focus on the bar
             setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-            childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_NO;
-        }
-        for (int i = 0; i < getChildCount(); i++) {
-            getChildAt(i).setImportantForAccessibility(childA11y);
-            // Only allowing focusing on bubbles when bar is expanded. Otherwise, in talkback mode,
-            // bubbles can be navigates to in collapsed mode.
-            getChildAt(i).setFocusable(mIsBarExpanded);
+            for (int i = 0; i < getChildCount(); i++) {
+                View childView = getChildAt(i);
+                childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+                childView.setFocusable(false);
+            }
         }
     }
 
@@ -1264,7 +1381,7 @@
         CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : "";
 
         // Don't count overflow if it exists
-        int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0);
+        int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0);
         if (bubbleCount > 1) {
             contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles,
                     contentDesc, bubbleCount - 1);
@@ -1272,6 +1389,26 @@
         setContentDescription(contentDesc);
     }
 
+    private void announceExpandedStateChange() {
+        final CharSequence selectedBubbleContentDesc;
+        if (mSelectedBubbleView != null) {
+            selectedBubbleContentDesc = mSelectedBubbleView.getContentDescription();
+        } else {
+            selectedBubbleContentDesc = getResources().getString(
+                    R.string.bubble_bar_bubble_fallback_description);
+        }
+
+        final String msg;
+        if (mIsBarExpanded) {
+            msg = getResources().getString(R.string.bubble_bar_accessibility_announce_expand,
+                    selectedBubbleContentDesc);
+        } else {
+            msg = getResources().getString(R.string.bubble_bar_accessibility_announce_collapse,
+                    selectedBubbleContentDesc);
+        }
+        announceForAccessibility(msg);
+    }
+
     private boolean isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding) {
         return isIconSizeUpdated(newIconSize) || isPaddingUpdated(newBubbleBarPadding);
     }
@@ -1316,15 +1453,16 @@
     public void dump(PrintWriter pw) {
         pw.println("BubbleBarView state:");
         pw.println("  visibility: " + getVisibility());
-        pw.println("  translation Y: " + getTranslationY());
-        pw.println("  bubbles in bar (childCount = " + getChildCount() + ")");
+        pw.println("  alpha: " + getAlpha());
+        pw.println("  translationY: " + getTranslationY());
+        pw.println("  childCount: " + getChildCount());
+        pw.println("  hasOverflow:  " + hasOverflow());
         for (BubbleView bubbleView: getBubbles()) {
             BubbleBarItem bubble = bubbleView.getBubble();
             String key = bubble == null ? "null" : bubble.getKey();
             pw.println("    bubble key: " + key);
         }
         pw.println("  isExpanded: " + isExpanded());
-        pw.println("  mIsAnimatingNewBubble: " + mIsAnimatingNewBubble);
         if (mBubbleAnimator != null) {
             pw.println("  mBubbleAnimator.isRunning(): " + mBubbleAnimator.isRunning());
             pw.println("  mBubbleAnimator is null");
@@ -1343,13 +1481,95 @@
         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 {
 
         /** Returns the translation Y that the bubble bar should have. */
         float getBubbleBarTranslationY();
 
-        /** Notifies the controller that the bubble bar was touched while it was animating. */
-        void onBubbleBarTouchedWhileAnimating();
+        /** Notifies the controller that the bubble bar was touched. */
+        void onBubbleBarTouched();
+
+        /** Requests the controller to expand bubble bar */
+        void expandBubbleBar();
+
+        /** Requests the controller to dismiss the bubble bar */
+        void dismissBubbleBar();
+
+        /** Requests the controller to update bubble bar location to the given value */
+        void updateBubbleBarLocation(BubbleBarLocation location,
+                @BubbleBarLocation.UpdateSource int source);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0f9de16..2dddb16 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -18,6 +18,12 @@
 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;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -27,21 +33,32 @@
 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;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 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.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.Flags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -58,10 +75,16 @@
     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;
 
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
@@ -69,53 +92,95 @@
     private BubbleDragController mBubbleDragController;
     private TaskbarStashController mTaskbarStashController;
     private TaskbarInsetsController mTaskbarInsetsController;
+    private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider;
     private View.OnClickListener mBubbleClickListener;
     private View.OnClickListener mBubbleBarClickListener;
+    private BubbleView.Controller mBubbleViewController;
+    private BubbleBarOverflow mOverflowBubble;
 
     // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
     private final MultiValueAlpha mBubbleBarAlpha;
-    private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale);
+    private final AnimatedFloat mBubbleBarBubbleAlpha = new AnimatedFloat(this::updateBubbleAlpha);
+    private final AnimatedFloat mBubbleBarBackgroundAlpha = new AnimatedFloat(
+            this::updateBackgroundAlpha);
+    private final AnimatedFloat mBubbleBarScaleX = new AnimatedFloat(this::updateScaleX);
+    private final AnimatedFloat mBubbleBarScaleY = new AnimatedFloat(this::updateScaleY);
+    private final AnimatedFloat mBubbleBarBackgroundScaleX = new AnimatedFloat(
+            this::updateBackgroundScaleX);
+    private final AnimatedFloat mBubbleBarBackgroundScaleY = new AnimatedFloat(
+            this::updateBackgroundScaleY);
     private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
             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;
+    // Modified when bubble bar is springing back into the stash handle.
+    private float mBubbleBarStashTranslationY;
 
     // Whether the bar is hidden for a sysui state.
     private boolean mHiddenForSysui;
     // Whether the bar is hidden because there are no bubbles.
     private boolean mHiddenForNoBubbles = true;
+    // Whether the bar is hidden when stashed
+    private boolean mHiddenForStashed;
     private boolean mShouldShowEducation;
 
+    public boolean mOverflowAdded;
+
     private BubbleBarViewAnimator mBubbleBarViewAnimator;
+    private final FrameLayout mBubbleBarContainer;
+    private BubbleBarFlyoutController mBubbleBarFlyoutController;
+    private TaskbarSharedState mTaskbarSharedState;
+    private final TimeSource mTimeSource = System::currentTimeMillis;
+    private final int mTaskbarTranslationDelta;
 
     @Nullable
     private BubbleBarBoundsChangeListener mBoundsChangeListener;
 
-    public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
+    public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView,
+            FrameLayout bubbleBarContainer) {
         mActivity = activity;
         mBarView = barView;
+        mBubbleBarContainer = bubbleBarContainer;
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
         mIconSize = activity.getResources().getDimensionPixelSize(
                 R.dimen.bubblebar_icon_size);
+        mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity);
     }
 
-    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+    /** 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;
-        mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
-
+        mBubbleBarFlyoutController = new BubbleBarFlyoutController(
+                mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks());
+        mBubbleBarViewAnimator = new BubbleBarViewAnimator(
+                mBarView, mBubbleStashController, mBubbleBarFlyoutController,
+                mBubbleBarController::showExpandedView);
+        mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
+        onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
-                dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true));
-        updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false);
-        mBubbleBarScale.updateValue(1f);
-        mBubbleClickListener = v -> onBubbleClicked(v);
-        mBubbleBarClickListener = v -> onBubbleBarClicked();
+                dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
+        mBubbleBarScaleY.updateValue(1f);
+        mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
+        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) -> {
@@ -124,6 +189,10 @@
                         mBoundsChangeListener.onBoundsChanged();
                     }
                 });
+        float pinningValue = DisplayController.isTransientTaskbar(mActivity)
+                ? PINNING_TRANSIENT
+                : PINNING_PERSISTENT;
+        mBubbleBarPinning.updateValue(pinningValue);
         mBarView.setController(new BubbleBarView.Controller() {
             @Override
             public float getBubbleBarTranslationY() {
@@ -131,14 +200,139 @@
             }
 
             @Override
-            public void onBubbleBarTouchedWhileAnimating() {
-                BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating();
+            public void onBubbleBarTouched() {
+                if (isAnimatingNewBubble()) {
+                    interruptAnimationForTouch();
+                }
+            }
+
+            @Override
+            public void expandBubbleBar() {
+                BubbleBarViewController.this.expandBubbleBar();
+            }
+
+            @Override
+            public void dismissBubbleBar() {
+                onDismissAllBubbles();
+            }
+
+            @Override
+            public void updateBubbleBarLocation(BubbleBarLocation location,
+                    @BubbleBarLocation.UpdateSource int source) {
+                mBubbleBarController.updateBubbleBarLocation(location, source);
             }
         });
+
+        mBubbleViewController = new BubbleView.Controller() {
+            @Override
+            public BubbleBarLocation getBubbleBarLocation() {
+                return BubbleBarViewController.this.getBubbleBarLocation();
+            }
+
+            @Override
+            public void dismiss(BubbleView bubble) {
+                if (bubble.getBubble() != null) {
+                    notifySysUiBubbleDismissed(bubble.getBubble());
+                }
+                onBubbleDismissed(bubble);
+            }
+
+            @Override
+            public void collapse() {
+                collapseBubbleBar();
+            }
+
+            @Override
+            public void updateBubbleBarLocation(BubbleBarLocation location,
+                    @BubbleBarLocation.UpdateSource int source) {
+                mBubbleBarController.updateBubbleBarLocation(location, source);
+            }
+        };
     }
 
-    private void onBubbleClicked(View v) {
-        BubbleBarItem bubble = ((BubbleView) v).getBubble();
+    /** 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) {
             Log.e(TAG, "bubble click listener, bubble was null");
         }
@@ -146,19 +340,19 @@
         final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
         if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
             // Tapping the currently selected bubble while expanded collapses the view.
-            setExpanded(false);
-            mBubbleStashController.stashBubbleBar();
+            collapseBubbleBar();
         } else {
             mBubbleBarController.showAndSelectBubble(bubble);
         }
     }
 
-    private void onBubbleBarTouchedWhileAnimating() {
-        mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating();
+    /** 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 onBubbleBarClicked() {
+    private void expandBubbleBar() {
         if (mShouldShowEducation) {
             mShouldShowEducation = false;
             // Get the bubble bar bounds on screen
@@ -177,6 +371,11 @@
         }
     }
 
+    private void collapseBubbleBar() {
+        setExpanded(false);
+        mBubbleStashController.stashBubbleBar();
+    }
+
     /** Notifies that the stash state is changing. */
     public void onStashStateChanging() {
         if (isAnimatingNewBubble()) {
@@ -193,19 +392,68 @@
         return mBubbleBarAlpha;
     }
 
-    public AnimatedFloat getBubbleBarScale() {
-        return mBubbleBarScale;
+    public AnimatedFloat getBubbleBarBubbleAlpha() {
+        return mBubbleBarBubbleAlpha;
+    }
+
+    public AnimatedFloat getBubbleBarBackgroundAlpha() {
+        return mBubbleBarBackgroundAlpha;
+    }
+
+    public AnimatedFloat getBubbleBarScaleX() {
+        return mBubbleBarScaleX;
+    }
+
+    public AnimatedFloat getBubbleBarScaleY() {
+        return mBubbleBarScaleY;
+    }
+
+    public AnimatedFloat getBubbleBarBackgroundScaleX() {
+        return mBubbleBarBackgroundScaleX;
+    }
+
+    public AnimatedFloat getBubbleBarBackgroundScaleY() {
+        return mBubbleBarBackgroundScaleY;
     }
 
     public AnimatedFloat getBubbleBarTranslationY() {
         return mBubbleBarTranslationY;
     }
 
-    float getBubbleBarCollapsedHeight() {
+    public AnimatedFloat getBubbleOffsetY() {
+        return mBubbleOffsetY;
+    }
+
+    public float getBubbleBarCollapsedWidth() {
+        return mBarView.collapsedWidth();
+    }
+
+    public float getBubbleBarCollapsedHeight() {
         return mBarView.getBubbleBarCollapsedHeight();
     }
 
     /**
+     * @see BubbleBarView#getRelativePivotX()
+     */
+    public float getBubbleBarRelativePivotX() {
+        return mBarView.getRelativePivotX();
+    }
+
+    /**
+     * @see BubbleBarView#getRelativePivotY()
+     */
+    public float getBubbleBarRelativePivotY() {
+        return mBarView.getRelativePivotY();
+    }
+
+    /**
+     * @see BubbleBarView#setRelativePivot(float, float)
+     */
+    public void setBubbleBarRelativePivot(float x, float y) {
+        mBarView.setRelativePivot(x, y);
+    }
+
+    /**
      * Whether the bubble bar is visible or not.
      */
     public boolean isBubbleBarVisible() {
@@ -225,6 +473,14 @@
     }
 
     /**
+     * @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on
+     * the right
+     */
+    public boolean isBubbleBarOnLeft() {
+        return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+    }
+
+    /**
      * Update bar {@link BubbleBarLocation}
      */
     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
@@ -247,9 +503,28 @@
         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;
+        final Rect bounds = getBubbleBarBounds();
+        final int bubbleBarTopOnScreen = mBarView.getRestingTopPositionOnScreen();
+        final float x = event.getX();
+        return event.getRawY() >= bubbleBarTopOnScreen && x >= bounds.left && x <= bounds.right;
+    }
+
     /** Whether a new bubble is animating. */
     public boolean isAnimatingNewBubble() {
-        return mBarView.isAnimatingNewBubble();
+        return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.isAnimating();
+    }
+
+    public boolean isNewBubbleAnimationRunningOrPending() {
+        return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimation();
     }
 
     /** The horizontal margin of the bubble bar from the edge of the screen. */
@@ -293,6 +568,7 @@
             if (hidden) {
                 mBarView.setAlpha(0);
                 mBarView.setExpanded(false);
+                adjustTaskbarAndHotseatToBubbleBarState(/* isBubbleBarExpanded = */ false);
             }
             mActivity.bubbleBarVisibilityChanged(!hidden);
         }
@@ -319,9 +595,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);
@@ -332,27 +616,66 @@
     // Modifying view related properties.
     //
 
-    private void updateBubbleBarIconSize(int newIconSize, boolean animate) {
+    /** Notifies controller of configuration change, so bubble bar can be adjusted */
+    public void onBubbleBarConfigurationChanged(boolean animate) {
+        int newIconSize;
+        int newPadding;
         Resources res = mActivity.getResources();
+        if (mBubbleStashController.isBubblesShowingOnHome()
+                || mBubbleStashController.isTransientTaskBar()) {
+            newIconSize = getBubbleBarIconSizeFromDeviceProfile(res);
+            newPadding = getBubbleBarPaddingFromDeviceProfile(res);
+        } else {
+            // the bubble bar is shown inside the persistent task bar, use preset sizes
+            newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
+            newPadding = res.getDimensionPixelSize(
+                    R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+        }
+        updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate);
+    }
+
+    private int getBubbleBarIconSizeFromDeviceProfile(Resources res) {
+        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);
         float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                 APP_ICON_MEDIUM_DP, dm);
-        float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
-                APP_ICON_LARGE_DP, dm);
         float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f;
-        float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
-        mIconSize = newIconSize <= smallMediumThreshold
+        int taskbarIconSize = deviceProfile.taskbarIconSize;
+        return taskbarIconSize <= smallMediumThreshold
                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) :
                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
-        float bubbleBarPadding = newIconSize >= mediumLargeThreshold
+
+    }
+
+    private int getBubbleBarPaddingFromDeviceProfile(Resources res) {
+        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);
+        float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                APP_ICON_LARGE_DP, dm);
+        float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
+        return deviceProfile.taskbarIconSize >= mediumLargeThreshold
                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) :
                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
+    }
+
+    private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) {
+        if (mIconSize == iconSize && mBubbleBarPadding == padding) return;
+        mIconSize = iconSize;
+        mBubbleBarPadding = padding;
         if (animate) {
-            mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding);
+            mBarView.animateBubbleBarIconSize(iconSize, padding);
         } else {
-            mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding);
+            mBarView.setIconSizeAndPadding(iconSize, padding);
         }
     }
 
@@ -364,20 +687,93 @@
         updateTranslationY();
     }
 
+    /**
+     * Sets the translation of the bubble bar during the stash animation.
+     */
+    public void setTranslationYForStash(float transY) {
+        mBubbleBarStashTranslationY = transY;
+        updateTranslationY();
+    }
+
     private void updateTranslationY() {
-        mBarView.setTranslationY(mBubbleBarTranslationY.value
-                + mBubbleBarSwipeUpTranslationY);
+        mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY
+                + 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);
     }
 
     /**
-     * Applies scale properties for the entire bubble bar.
+     * Calculates the vertical difference in the bubble bar positions for pinned and transient
+     * taskbar modes.
      */
-    private void updateScale() {
-        float scale = mBubbleBarScale.value;
+    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) {
         mBarView.setScaleX(scale);
+    }
+
+    private void updateScaleY(float scale) {
         mBarView.setScaleY(scale);
     }
 
+    private void updateBackgroundScaleX(float scale) {
+        mBarView.setBackgroundScaleX(scale);
+    }
+
+    private void updateBackgroundScaleY(float scale) {
+        mBarView.setBackgroundScaleY(scale);
+    }
+
+    private void updateBubbleAlpha(float alpha) {
+        mBarView.setBubbleAlpha(alpha);
+    }
+
+    private void updateBubbleOffsetY(float transY) {
+        mBarView.setBubbleOffsetY(transY);
+    }
+
+    private void updateBackgroundAlpha(float alpha) {
+        mBarView.setBackgroundAlpha(alpha);
+    }
+
     //
     // Manipulating the specific bubble views in the bar
     //
@@ -388,6 +784,7 @@
     public void removeBubble(BubbleBarBubble b) {
         if (b != null) {
             mBarView.removeBubble(b.getView());
+            b.getView().setController(null);
         } else {
             Log.w(TAG, "removeBubble, bubble was null!");
         }
@@ -395,15 +792,61 @@
 
     /** Adds a new bubble and removes an old bubble at the same time. */
     public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble,
-            BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation) {
-        mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView());
+            BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation,
+            boolean addOverflowToo) {
+        mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(),
+                addOverflowToo ? () -> showOverflow(true) : null);
         addedBubble.getView().setOnClickListener(mBubbleClickListener);
+        addedBubble.getView().setController(mBubbleViewController);
+        removedBubble.getView().setController(null);
         mBubbleDragController.setupBubbleView(addedBubble.getView());
         if (!suppressAnimation) {
             animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false);
         }
     }
 
+    /** Whether the overflow view is added to the bubble bar. */
+    public boolean isOverflowAdded() {
+        return mOverflowAdded;
+    }
+
+    /** Shows or hides the overflow view. */
+    public void showOverflow(boolean showOverflow) {
+        if (mOverflowAdded == showOverflow) return;
+        mOverflowAdded = showOverflow;
+        if (mOverflowAdded) {
+            mBarView.addBubble(mOverflowBubble.getView());
+            mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
+            mOverflowBubble.getView().setController(mBubbleViewController);
+        } else {
+            mBarView.removeBubble(mOverflowBubble.getView());
+            mOverflowBubble.getView().setOnClickListener(null);
+            mOverflowBubble.getView().setController(null);
+        }
+    }
+
+    /** Adds the overflow view to the bubble bar while animating a view away. */
+    public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) {
+        if (mOverflowAdded) return;
+        mOverflowAdded = true;
+        mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(),
+                null /* onEndRunnable */);
+        mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
+        mOverflowBubble.getView().setController(mBubbleViewController);
+        removedBubble.getView().setController(null);
+    }
+
+    /** Removes the overflow view to the bubble bar while animating a view in. */
+    public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) {
+        if (!mOverflowAdded) return;
+        mOverflowAdded = false;
+        mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(),
+                null /* onEndRunnable */);
+        addedBubble.getView().setOnClickListener(mBubbleClickListener);
+        addedBubble.getView().setController(mBubbleViewController);
+        mOverflowBubble.getView().setController(null);
+    }
+
     /**
      * Adds the provided bubble to the bubble bar.
      */
@@ -412,16 +855,15 @@
             mBarView.addBubble(b.getView());
             b.getView().setOnClickListener(mBubbleClickListener);
             mBubbleDragController.setupBubbleView(b.getView());
-
-            if (b instanceof BubbleBarOverflow) {
-                return;
-            }
+            b.getView().setController(mBubbleViewController);
 
             if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) {
                 // the bubble bar and handle are initialized as part of the first bubble animation.
                 // if the animation is suppressed, immediately stash or show the bubble bar to
                 // ensure they've been initialized.
-                if (mTaskbarStashController.isInApp()) {
+                if (mTaskbarStashController.isInApp()
+                        && mBubbleStashController.isTransientTaskBar()
+                        && mTaskbarStashController.isStashed()) {
                     mBubbleStashController.stashBubbleBarImmediate();
                 } else {
                     mBubbleStashController.showBubbleBarImmediate();
@@ -437,22 +879,32 @@
     /** 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. one bubble is the overflow
-        // so check for at most 2 children.
-        if (mBarView.getChildCount() <= 2 && !isUpdate) {
+        // if this is the first bubble, animate to the initial state.
+        if (mBarView.getBubbleChildCount() == 1 && !isUpdate) {
             mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
             return;
         }
-
-        if (mBubbleStashController.isBubblesShowingOnHome() && !isExpanding && !isExpanded()) {
-            mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble);
+        // if we're not stashed or we're in persistent taskbar, animate for collapsed state.
+        boolean animateForCollapsed = !mBubbleStashController.isStashed()
+                || !mBubbleStashController.isTransientTaskBar();
+        if (animateForCollapsed) {
+            mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
             return;
         }
 
-        // only animate the new bubble if we're in an app and not auto expanding
-        if (isInApp && !isExpanding && !isExpanded()) {
-            mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
+        if (isInApp && mBubbleStashController.getHasHandleView()) {
+            mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
         }
     }
 
@@ -479,8 +931,9 @@
      * from Launcher.
      */
     public void setExpanded(boolean isExpanded) {
-        if (isExpanded != mBarView.isExpanded()) {
+        if (!mBubbleBarPinning.isAnimating() && isExpanded != mBarView.isExpanded()) {
             mBarView.setExpanded(isExpanded);
+            adjustTaskbarAndHotseatToBubbleBarState(isExpanded);
             if (!isExpanded) {
                 mSystemUiProxy.collapseBubbles();
             } else {
@@ -492,10 +945,44 @@
     }
 
     /**
+     * Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in
+     * app or overview.
+     */
+    private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) {
+        if (!mBubbleStashController.isBubblesShowingOnHome()
+                && !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();
+        }
+    }
+
+    /** Return {@code true} if expanded bubble bar would intersect the taskbar. */
+    public boolean isIntersectingTaskbar() {
+        if (mBarView.isExpanding() || mBarView.isExpanded()) {
+            Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds();
+            return mBarView.getBubbleBarExpandedBounds().intersect(taskbarViewBounds);
+        } else {
+            return false;
+        }
+    }
+
+    /**
      * Sets whether the bubble bar should be expanded. This method is used in response to UI events
      * from SystemUI.
      */
     public void setExpandedFromSysui(boolean isExpanded) {
+        if (isNewBubbleAnimationRunningOrPending() && isExpanded) {
+            mBubbleBarViewAnimator.expandedWhileAnimating();
+            return;
+        }
         if (!isExpanded) {
             mBubbleStashController.stashBubbleBar();
         } else {
@@ -512,6 +999,7 @@
     /**
      * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
      * that a bubble is being dragged to dismiss.
+     *
      * @param bubbleView dragged bubble view
      */
     public void onBubbleDragStart(@NonNull BubbleView bubbleView) {
@@ -528,8 +1016,8 @@
         mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
     }
 
-    /** Notifies {@link BubbleBarView} that the dragged bubble was dismissed. */
-    public void onBubbleDragDismissed(BubbleView bubble) {
+    /** Handle given bubble being dismissed */
+    public void onBubbleDismissed(BubbleView bubble) {
         mBubbleBarController.onBubbleDismissed(bubble);
         mBarView.removeBubble(bubble);
     }
@@ -572,20 +1060,29 @@
     }
 
     /**
-     * Called when bubble was dragged into the dismiss target. Notifies System
-     * @param bubble dismissed bubble item
+     * Notify SystemUI that the given bubble has been dismissed.
      */
-    public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
-        mSystemUiProxy.dragBubbleToDismiss(bubble.getKey());
+    public void notifySysUiBubbleDismissed(@NonNull BubbleBarItem bubble) {
+        mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
     }
 
     /**
-     * Called when bubble stack was dragged into the dismiss target
+     * Called when bubble stack was dismissed
      */
-    public void onDismissAllBubblesWhileDragging() {
+    public void onDismissAllBubbles() {
         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
      */
@@ -593,6 +1090,53 @@
         mBoundsChangeListener = listener;
     }
 
+    /** Called when the controller is destroyed. */
+    public void onDestroy() {
+        adjustTaskbarAndHotseatToBubbleBarState(/*isBubbleBarExpanded = */false);
+    }
+
+    /**
+     * Create an animator for showing or hiding bubbles when stashed state changes
+     *
+     * @param isStashed {@code true} when bubble bar should be stashed to the handle
+     */
+    public Animator createRevealAnimatorForStashChange(boolean isStashed) {
+        Rect stashedHandleBounds = new Rect();
+        mBubbleStashController.getHandleBounds(stashedHandleBounds);
+        int childCount = mBarView.getChildCount();
+        float newChildWidth = (float) stashedHandleBounds.width() / childCount;
+        AnimatorSet animatorSet = new AnimatorSet();
+        for (int i = 0; i < childCount; i++) {
+            BubbleView child = (BubbleView) mBarView.getChildAt(i);
+            animatorSet.play(
+                    createRevealAnimForBubble(child, isStashed, stashedHandleBounds,
+                            newChildWidth));
+        }
+        return animatorSet;
+    }
+
+    private Animator createRevealAnimForBubble(BubbleView bubbleView, boolean isStashed,
+            Rect stashedHandleBounds, float newWidth) {
+        Rect viewBounds = new Rect(0, 0, bubbleView.getWidth(), bubbleView.getHeight());
+
+        int viewCenterY = viewBounds.centerY();
+        int halfHandleHeight = stashedHandleBounds.height() / 2;
+        int widthDelta = Math.max(0, (int) (viewBounds.width() - newWidth) / 2);
+
+        Rect stashedViewBounds = new Rect(
+                viewBounds.left + widthDelta,
+                viewCenterY - halfHandleHeight,
+                viewBounds.right - widthDelta,
+                viewCenterY + halfHandleHeight
+        );
+
+        float viewRadius = 0f; // Use 0 to not clip the new message dot or the app icon
+        float stashedRadius = stashedViewBounds.height() / 2f;
+
+        return new RoundedRectRevealOutlineProvider(viewRadius, stashedRadius, viewBounds,
+                stashedViewBounds).createRevealAnimator(bubbleView, !isStashed, 0);
+    }
+
     /**
      * Listener to receive updates about bubble bar bounds changing
      */
@@ -601,6 +1145,11 @@
         void onBoundsChanged();
     }
 
+    /** Interface for getting the current timestamp. */
+    interface TimeSource {
+        long currentTimeMillis();
+    }
+
     /** Dumps the state of BubbleBarViewController. */
     public void dump(PrintWriter pw) {
         pw.println("Bubble bar view controller state:");
@@ -609,10 +1158,21 @@
         pw.println("  mShouldShowEducation: " + mShouldShowEducation);
         pw.println("  mBubbleBarTranslationY.value: " + mBubbleBarTranslationY.value);
         pw.println("  mBubbleBarSwipeUpTranslationY: " + mBubbleBarSwipeUpTranslationY);
+        pw.println("  mOverflowAdded: " + mOverflowAdded);
         if (mBarView != null) {
             mBarView.dump(pw);
         } else {
             pw.println("  Bubble bar view is null!");
         }
     }
+
+    /** Interface for BubbleBarViewController to get the taskbar view properties. */
+    public interface TaskbarViewPropertiesProvider {
+
+        /** Returns the bounds of the taskbar. */
+        Rect getTaskbarViewBounds();
+
+        /** Returns taskbar icons alpha */
+        MultiPropertyFactory<View>.MultiProperty getIconsAlpha();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 03140fe..d993685 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -15,24 +15,35 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
+import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_BUBBLE_BAR;
+
+import android.graphics.Rect;
+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;
 
 import java.io.PrintWriter;
+import java.util.Optional;
 
-/**
- * Hosts various bubble controllers to facilitate passing between one another.
- */
+/** Hosts various bubble controllers to facilitate passing between one another. */
 public class BubbleControllers {
 
     public final BubbleBarController bubbleBarController;
     public final BubbleBarViewController bubbleBarViewController;
     public final BubbleStashController bubbleStashController;
-    public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
+    public final Optional<BubbleStashedHandleViewController> bubbleStashedHandleViewController;
     public final BubbleDragController bubbleDragController;
     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();
 
@@ -45,11 +56,13 @@
             BubbleBarController bubbleBarController,
             BubbleBarViewController bubbleBarViewController,
             BubbleStashController bubbleStashController,
-            BubbleStashedHandleViewController bubbleStashedHandleViewController,
+            Optional<BubbleStashedHandleViewController> bubbleStashedHandleViewController,
             BubbleDragController bubbleDragController,
             BubbleDismissController bubbleDismissController,
             BubbleBarPinController bubbleBarPinController,
-            BubblePinController bubblePinController) {
+            BubblePinController bubblePinController,
+            Optional<BubbleBarSwipeController> bubbleBarSwipeController,
+            BubbleCreator bubbleCreator) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
         this.bubbleStashController = bubbleStashController;
@@ -58,6 +71,8 @@
         this.bubbleDismissController = bubbleDismissController;
         this.bubbleBarPinController = bubbleBarPinController;
         this.bubblePinController = bubblePinController;
+        this.bubbleBarSwipeController = bubbleBarSwipeController;
+        this.bubbleCreator = bubbleCreator;
     }
 
     /**
@@ -65,16 +80,44 @@
      * 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) {
+    public void init(TaskbarSharedState taskbarSharedState, TaskbarControllers taskbarControllers) {
+        BubbleBarLocationCompositeListener bubbleBarLocationListeners =
+                new BubbleBarLocationCompositeListener(
+                        taskbarControllers.navbarButtonsViewController,
+                        taskbarControllers.taskbarViewController,
+                        new BubbleBarLocationOnDemandListener(() -> taskbarControllers.uiController)
+                );
         bubbleBarController.init(this,
-                taskbarControllers.navbarButtonsViewController::isImeVisible);
-        bubbleBarViewController.init(taskbarControllers, this);
-        bubbleStashedHandleViewController.init(taskbarControllers, this);
-        bubbleStashController.init(taskbarControllers, this);
+                bubbleBarLocationListeners,
+                taskbarControllers.navbarButtonsViewController::isImeVisible,
+                taskbarSharedState);
+        bubbleStashedHandleViewController.ifPresent(
+                controller -> controller.init(/* bubbleControllers = */ this));
+        bubbleStashController.init(
+                taskbarControllers.taskbarInsetsController,
+                bubbleBarViewController,
+                bubbleStashedHandleViewController.orElse(null),
+                taskbarControllers::runAfterInit
+        );
+        bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this,
+                new TaskbarViewPropertiesProvider() {
+                    @Override
+                    public Rect getTaskbarViewBounds() {
+                        return taskbarControllers.taskbarViewController.getIconLayoutBounds();
+                    }
+
+                    @Override
+                    public MultiPropertyFactory<View>.MultiProperty getIconsAlpha() {
+                        return taskbarControllers.taskbarViewController
+                                .getTaskbarIconAlpha()
+                                .get(ALPHA_INDEX_BUBBLE_BAR);
+                    }
+                });
         bubbleDragController.init(/* bubbleControllers = */ this);
         bubbleDismissController.init(/* bubbleControllers = */ this);
-        bubbleBarPinController.init(this);
+        bubbleBarPinController.init(this, bubbleBarLocationListeners);
         bubblePinController.init(this);
+        bubbleBarSwipeController.ifPresent(c -> c.init(this));
 
         mPostInitRunnables.executeAllAndDestroy();
     }
@@ -93,8 +136,9 @@
      * Cleans up all controllers.
      */
     public void onDestroy() {
-        bubbleStashedHandleViewController.onDestroy();
+        bubbleStashedHandleViewController.ifPresent(BubbleStashedHandleViewController::onDestroy);
         bubbleBarController.onDestroy();
+        bubbleBarViewController.onDestroy();
     }
 
     /** Dumps bubble controllers state. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
new file mode 100644
index 0000000..c5efe2f
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
@@ -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.bubbles;
+
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
+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;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.PathParser;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+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).
+ */
+public class BubbleCreator {
+
+    private static final String TAG = BubbleCreator.class.getSimpleName();
+
+    private final Context mContext;
+    private final LauncherApps mLauncherApps;
+    private final BubbleIconFactory mIconFactory;
+
+    public BubbleCreator(Context context) {
+        mContext = context;
+        mLauncherApps = mContext.getSystemService(LauncherApps.class);
+        mIconFactory = new BubbleIconFactory(context,
+                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
+                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
+                context.getResources().getColor(R.color.important_conversation),
+                context.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.importance_ring_stroke_width));
+    }
+
+    /**
+     * Creates a BubbleBarBubble object, including the view if needed, and populates it with
+     * the info needed for presentation.
+     *
+     * @param context the context to use for inflation.
+     * @param info the info to use to populate the bubble.
+     * @param barView the parent view for the bubble (bubble is not added to the view).
+     * @param existingBubble if a bubble exists already, this object gets updated with the new
+     *                       info & returned (& any existing views are reused instead of inflating
+     *                       new ones.
+     */
+    @Nullable
+    public BubbleBarBubble populateBubble(Context context, BubbleInfo info, ViewGroup barView,
+            @Nullable BubbleBarBubble existingBubble) {
+        String appName;
+        Bitmap badgeBitmap;
+        Bitmap bubbleBitmap;
+        Path dotPath;
+        int dotColor;
+
+        boolean isImportantConvo = info.isImportantConversation();
+
+        ShortcutRequest.QueryResult result = new ShortcutRequest(context,
+                new UserHandle(info.getUserId()))
+                .forPackage(info.getPackageName(), info.getShortcutId())
+                .query(FLAG_MATCH_DYNAMIC
+                        | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
+                        | FLAG_MATCH_CACHED
+                        | FLAG_GET_PERSONS_DATA);
+
+        ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
+        if (shortcutInfo == null) {
+            Log.w(TAG, "No shortcutInfo found for bubble: " + info.getKey()
+                    + " with shortcutId: " + info.getShortcutId());
+        }
+
+        ApplicationInfo appInfo;
+        try {
+            appInfo = mLauncherApps.getApplicationInfo(
+                    info.getPackageName(),
+                    0,
+                    new UserHandle(info.getUserId()));
+        } catch (PackageManager.NameNotFoundException e) {
+            // If we can't find package... don't think we should show the bubble.
+            Log.w(TAG, "Unable to find packageName: " + info.getPackageName());
+            return null;
+        }
+        if (appInfo == null) {
+            Log.w(TAG, "Unable to find appInfo: " + info.getPackageName());
+            return null;
+        }
+        PackageManager pm = context.getPackageManager();
+        appName = String.valueOf(appInfo.loadLabel(pm));
+        Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
+        Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(info.getUserId()));
+
+        // Badged bubble image
+        Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
+                info.getIcon());
+        if (bubbleDrawable == null) {
+            // Default to app icon
+            bubbleDrawable = appIcon;
+        }
+
+        BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
+        badgeBitmap = badgeBitmapInfo.icon;
+
+        float[] bubbleBitmapScale = new float[1];
+        bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
+
+        // Dot color & placement
+        Path iconPath = PathParser.createPathFromPathData(
+                context.getResources().getString(
+                        com.android.internal.R.string.config_icon_mask));
+        Matrix matrix = new Matrix();
+        float scale = bubbleBitmapScale[0];
+        float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
+        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+                radius /* pivot y */);
+        iconPath.transform(matrix);
+        dotPath = iconPath;
+        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, flyoutMessage);
+            bubbleView.setBubble(bubble);
+            return bubble;
+        } else {
+            // If we already have a bubble (so it already has an inflated view), update it.
+            existingBubble.setInfo(info);
+            existingBubble.setBadge(badgeBitmap);
+            existingBubble.setIcon(bubbleBitmap);
+            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.
+     *
+     * @param barView the parent view for the bubble (bubble is not added to the view).
+     */
+    public BubbleBarOverflow createOverflow(ViewGroup barView) {
+        Bitmap bitmap = createOverflowBitmap();
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        BubbleView bubbleView = (BubbleView) inflater.inflate(
+                R.layout.bubble_bar_overflow_button, barView, false /* attachToRoot */);
+        BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
+        bubbleView.setOverflow(overflow, bitmap);
+        return overflow;
+    }
+
+    private Bitmap createOverflowBitmap() {
+        Drawable iconDrawable = mContext.getDrawable(R.drawable.bubble_ic_overflow_button);
+
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[]{
+                        R.attr.materialColorOnPrimaryFixed,
+                        R.attr.materialColorPrimaryFixed
+                });
+        int overflowIconColor = ta.getColor(0, Color.WHITE);
+        int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
+        ta.recycle();
+
+        iconDrawable.setTint(overflowIconColor);
+
+        int inset = mContext.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
+        Drawable foreground = new InsetDrawable(iconDrawable, inset);
+        Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
+                foreground);
+
+        return mIconFactory.createBadgedIconBitmap(drawable).icon;
+    }
+
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index a6096e2..a459dd9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -29,8 +29,8 @@
 import com.android.launcher3.R;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarDragLayer;
-import com.android.wm.shell.common.bubbles.DismissView;
-import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.shared.bubbles.DismissView;
+import com.android.wm.shell.shared.magnetictarget.MagnetizedObject;
 
 /**
  * Controls dismiss view presentation for the bubble bar dismiss functionality.
@@ -143,10 +143,10 @@
         if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
             BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
             if (bubbleView.getBubble() != null) {
-                mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+                mBubbleBarViewController.notifySysUiBubbleDismissed(bubbleView.getBubble());
             }
         } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
-            mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+            mBubbleBarViewController.onDismissAllBubbles();
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
index 6c3f0d8..a8002a5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
@@ -18,7 +18,7 @@
 package com.android.launcher3.taskbar.bubbles
 
 import com.android.launcher3.R
-import com.android.wm.shell.common.bubbles.DismissView
+import com.android.wm.shell.shared.bubbles.DismissView
 
 /**
  * Dismiss view is shared from WMShell. It requires setup with local resources.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
index 7aed2d2..adaba7a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -29,8 +29,8 @@
 import androidx.dynamicanimation.animation.FloatPropertyCompat;
 
 import com.android.launcher3.R;
-import com.android.wm.shell.common.bubbles.DismissCircleView;
-import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.shared.bubbles.DismissCircleView;
+import com.android.wm.shell.shared.bubbles.DismissView;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 /**
@@ -128,7 +128,7 @@
                         boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
                         boolean allRelevantPropertyAnimationsEnded) -> {
                     if (canceled || allRelevantPropertyAnimationsEnded) {
-                        resetAnimatedViews(restingPosition);
+                        resetAnimatedViews(restingPosition, /* dismissed= */ false);
                         if (endActions != null) {
                             endActions.run();
                         }
@@ -197,7 +197,7 @@
                         boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
                         boolean allRelevantPropertyAnimationsEnded) -> {
                     if (canceled || allRelevantPropertyAnimationsEnded) {
-                        resetAnimatedViews(initialPosition);
+                        resetAnimatedViews(initialPosition, /* dismissed= */ true);
                         if (endActions != null) endActions.run();
                     }
                 })
@@ -208,11 +208,14 @@
      * Reset the animated views to the initial state
      *
      * @param initialPosition position of the bubble
+     * @param dismissed whether the animated view was dismissed
      */
-    private void resetAnimatedViews(@NonNull PointF initialPosition) {
+    private void resetAnimatedViews(@NonNull PointF initialPosition, boolean dismissed) {
         mView.setScaleX(1f);
         mView.setScaleY(1f);
-        mView.setAlpha(1f);
+        if (!dismissed) {
+            mView.setAlpha(1f);
+        }
         mView.setTranslationX(initialPosition.x);
         mView.setTranslationY(initialPosition.y);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 8316b5b..fd4cf0e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -27,8 +27,8 @@
 import androidx.dynamicanimation.animation.FloatPropertyCompat;
 
 import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 /**
  * Controls bubble bar drag interactions.
@@ -77,6 +77,8 @@
     private BubbleBarPinController mBubbleBarPinController;
     private BubblePinController mBubblePinController;
 
+    private boolean mIsDragging;
+
     public BubbleDragController(TaskbarActivityContext activity) {
         mActivity = activity;
     }
@@ -153,13 +155,14 @@
             @Override
             protected void onDragDismiss() {
                 mBubblePinController.onDragEnd();
-                mBubbleBarViewController.onBubbleDragDismissed(bubbleView);
+                mBubbleBarViewController.onBubbleDismissed(bubbleView);
                 mBubbleBarViewController.onBubbleDragEnd();
             }
 
             @Override
             void onDragEnd() {
-                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
+                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation,
+                        BubbleBarLocation.UpdateSource.DRAG_BUBBLE);
                 mBubbleBarViewController.onBubbleDragEnd();
                 mBubblePinController.setListener(null);
             }
@@ -224,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);
@@ -240,6 +244,16 @@
         });
     }
 
+    /** Whether there is an item being dragged or not. */
+    public boolean isDragging() {
+        return mIsDragging;
+    }
+
+    /** Sets whether something is being dragged or not. */
+    public void setIsDragging(boolean isDragging) {
+        mIsDragging = isDragging;
+    }
+
     /**
      * Bubble touch listener for handling a single bubble view or bubble bar view while dragging.
      * The dragging starts after "shorter" long click (the long click duration might change):
@@ -436,6 +450,7 @@
 
         private void startDragging(@NonNull View view) {
             onDragStart();
+            BubbleDragController.this.setIsDragging(true);
             mActivity.setTaskbarWindowFullscreen(true);
             mAnimator = new BubbleDragAnimator(view);
             mAnimator.animateFocused();
@@ -452,6 +467,7 @@
         }
 
         private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
+            BubbleDragController.this.setIsDragging(false);
             Runnable onComplete = () -> {
                 mActivity.setTaskbarWindowFullscreen(false);
                 cleanUp(view);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
index a77e685..af1666f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
@@ -27,8 +27,9 @@
 import android.widget.FrameLayout
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.R
-import com.android.wm.shell.common.bubbles.BaseBubblePinController
-import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.wm.shell.shared.bubbles.BaseBubblePinController
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 
 /** Controller to manage pinning bubble bar to left or right when dragging starts from a bubble */
 class BubblePinController(
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
deleted file mode 100644
index 74f58ac..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ /dev/null
@@ -1,470 +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.taskbar.bubbles;
-
-import static java.lang.Math.abs;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.annotation.Nullable;
-import android.view.InsetsController;
-import android.view.MotionEvent;
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.taskbar.StashedHandleViewController;
-import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.launcher3.taskbar.TaskbarControllers;
-import com.android.launcher3.taskbar.TaskbarInsetsController;
-import com.android.launcher3.taskbar.TaskbarStashController;
-import com.android.launcher3.util.MultiPropertyFactory;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
-import com.android.wm.shell.shared.animation.PhysicsAnimator;
-
-import java.io.PrintWriter;
-
-/**
- * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to
- * create a cohesive animation between stashed/unstashed states.
- */
-public class BubbleStashController {
-
-    private static final String TAG = "BubbleStashController";
-
-    /**
-     * How long to stash/unstash.
-     */
-    public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
-
-    /**
-     * The scale bubble bar animates to when being stashed.
-     */
-    private static final float STASHED_BAR_SCALE = 0.5f;
-
-    protected final TaskbarActivityContext mActivity;
-
-    // Initialized in init.
-    private TaskbarControllers mControllers;
-    private TaskbarInsetsController mTaskbarInsetsController;
-    private BubbleBarViewController mBarViewController;
-    private BubbleStashedHandleViewController mHandleViewController;
-    private TaskbarStashController mTaskbarStashController;
-
-    private MultiPropertyFactory.MultiProperty mIconAlphaForStash;
-    private AnimatedFloat mIconScaleForStash;
-    private AnimatedFloat mIconTranslationYForStash;
-    private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha;
-
-    private boolean mRequestedStashState;
-    private boolean mRequestedExpandedState;
-
-    private boolean mIsStashed;
-    private int mStashedHeight;
-    private int mUnstashedHeight;
-    private boolean mBubblesShowingOnHome;
-    private boolean mBubblesShowingOnOverview;
-    private boolean mIsSysuiLocked;
-
-    private final float mHandleCenterFromScreenBottom;
-
-    @Nullable
-    private AnimatorSet mAnimator;
-
-    public BubbleStashController(TaskbarActivityContext activity) {
-        mActivity = activity;
-        // the handle is centered within the stashed taskbar area
-        mHandleCenterFromScreenBottom =
-                mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f;
-    }
-
-    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
-        mControllers = controllers;
-        mTaskbarInsetsController = controllers.taskbarInsetsController;
-        mBarViewController = bubbleControllers.bubbleBarViewController;
-        mHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
-        mTaskbarStashController = controllers.taskbarStashController;
-
-        mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0);
-        mIconScaleForStash = mBarViewController.getBubbleBarScale();
-        mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY();
-
-        mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get(
-                StashedHandleViewController.ALPHA_INDEX_STASHED);
-
-        mStashedHeight = mHandleViewController.getStashedHeight();
-        mUnstashedHeight = mHandleViewController.getUnstashedHeight();
-    }
-
-    /**
-     * Returns the touchable height of the bubble bar based on it's stashed state.
-     */
-    public int getTouchableHeight() {
-        return mIsStashed ? mStashedHeight : mUnstashedHeight;
-    }
-
-    /**
-     * Returns whether the bubble bar is currently stashed.
-     */
-    public boolean isStashed() {
-        return mIsStashed;
-    }
-
-    /**
-     * Animates the handle (or bubble bar depending on state) to be visible after the device is
-     * unlocked.
-     *
-     * <p>Normally either the bubble bar or the handle is visible,
-     * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
-     * between these two states. But the transition from the state where both the bar and handle
-     * are invisible is slightly different.
-     */
-    private void animateAfterUnlock() {
-        AnimatorSet animatorSet = new AnimatorSet();
-        if (mBubblesShowingOnHome || mBubblesShowingOnOverview) {
-            mIsStashed = false;
-            animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
-                    mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
-                    mIconAlphaForStash.animateToValue(1));
-        } else {
-            mIsStashed = true;
-            animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1));
-        }
-
-        animatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                onIsStashedChanged();
-            }
-        });
-        animatorSet.setDuration(BAR_STASH_DURATION).start();
-    }
-
-    /**
-     * Called when launcher enters or exits the home page. Bubbles are unstashed on home.
-     */
-    public void setBubblesShowingOnHome(boolean onHome) {
-        if (mBubblesShowingOnHome != onHome) {
-            mBubblesShowingOnHome = onHome;
-
-            if (!mBarViewController.hasBubbles()) {
-                // if there are no bubbles, there's nothing to show, so just return.
-                return;
-            }
-
-            if (mBubblesShowingOnHome) {
-                showBubbleBar(/* expanded= */ false);
-                // When transitioning from app to home the stash animator may already have been
-                // created, so we need to animate the bubble bar here to align with hotseat.
-                if (!mIsStashed) {
-                    mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat())
-                            .start();
-                }
-                // If the bubble bar is already unstashed, the taskbar touchable region won't be
-                // updated correctly, so force an update here.
-                mControllers.runAfterInit(() ->
-                        mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged());
-            } else if (!mBarViewController.isExpanded()) {
-                stashBubbleBar();
-            }
-        }
-    }
-
-    /** Whether bubbles are showing on the launcher home page. */
-    public boolean isBubblesShowingOnHome() {
-        boolean hasBubbles = mBarViewController != null && mBarViewController.hasBubbles();
-        return mBubblesShowingOnHome && hasBubbles;
-    }
-
-    // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing
-    /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */
-    public void setBubblesShowingOnOverview(boolean onOverview) {
-        if (mBubblesShowingOnOverview != onOverview) {
-            mBubblesShowingOnOverview = onOverview;
-            if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) {
-                stashBubbleBar();
-            } else {
-                // When transitioning to overview the stash animator may already have been
-                // created, so we need to animate the bubble bar here to align with taskbar.
-                mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar())
-                        .start();
-            }
-        }
-    }
-
-    /** Whether bubbles are showing on Overview. */
-    public boolean isBubblesShowingOnOverview() {
-        return mBubblesShowingOnOverview;
-    }
-
-    /** Called when sysui locked state changes, when locked, bubble bar is stashed. */
-    public void onSysuiLockedStateChange(boolean isSysuiLocked) {
-        if (isSysuiLocked != mIsSysuiLocked) {
-            mIsSysuiLocked = isSysuiLocked;
-            if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
-                animateAfterUnlock();
-            }
-        }
-    }
-
-    /**
-     * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the
-     * bar does not stash).
-     */
-    public void stashBubbleBar() {
-        mRequestedStashState = true;
-        mRequestedExpandedState = false;
-        updateStashedAndExpandedState();
-    }
-
-    /**
-     * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}.
-     */
-    public void showBubbleBar(boolean expandBubbles) {
-        mRequestedStashState = false;
-        mRequestedExpandedState = expandBubbles;
-        updateStashedAndExpandedState();
-    }
-
-    private void updateStashedAndExpandedState() {
-        if (mBarViewController.isHiddenForNoBubbles()) {
-            // If there are no bubbles the bar and handle are invisible, nothing to do here.
-            return;
-        }
-        boolean isStashed = mRequestedStashState
-                && !mBubblesShowingOnHome
-                && !mBubblesShowingOnOverview;
-        if (mIsStashed != 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
-            mBarViewController.onStashStateChanging();
-            mIsStashed = isStashed;
-            if (mAnimator != null) {
-                mAnimator.cancel();
-            }
-            mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION);
-            mAnimator.start();
-            onIsStashedChanged();
-        }
-        if (mBarViewController.isExpanded() != mRequestedExpandedState) {
-            mBarViewController.setExpanded(mRequestedExpandedState);
-        }
-    }
-
-    /**
-     * Create a stash animation.
-     *
-     * @param isStashed whether it's a stash animation or an unstash animation
-     * @param duration duration of the animation
-     * @return the animation
-     */
-    private AnimatorSet createStashAnimator(boolean isStashed, long duration) {
-        AnimatorSet animatorSet = new AnimatorSet();
-
-        AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
-        // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
-        AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
-        AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
-
-        final float firstHalfDurationScale;
-        final float secondHalfDurationScale;
-
-        if (isStashed) {
-            firstHalfDurationScale = 0.75f;
-            secondHalfDurationScale = 0.5f;
-
-            fullLengthAnimatorSet.play(
-                    mIconTranslationYForStash.animateToValue(getStashTranslation()));
-
-            firstHalfAnimatorSet.playTogether(
-                    mIconAlphaForStash.animateToValue(0),
-                    mIconScaleForStash.animateToValue(STASHED_BAR_SCALE));
-            secondHalfAnimatorSet.playTogether(
-                    mBubbleStashedHandleAlpha.animateToValue(1));
-        } else  {
-            firstHalfDurationScale = 0.5f;
-            secondHalfDurationScale = 0.75f;
-
-            final float translationY = getBubbleBarTranslationY();
-
-            fullLengthAnimatorSet.playTogether(
-                    mIconScaleForStash.animateToValue(1),
-                    mIconTranslationYForStash.animateToValue(translationY));
-
-            firstHalfAnimatorSet.playTogether(
-                    mBubbleStashedHandleAlpha.animateToValue(0)
-            );
-            secondHalfAnimatorSet.playTogether(
-                    mIconAlphaForStash.animateToValue(1)
-            );
-        }
-
-        fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed));
-
-        fullLengthAnimatorSet.setDuration(duration);
-        firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
-        secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
-        secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
-
-        animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
-                secondHalfAnimatorSet);
-        animatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimator = null;
-                mControllers.runAfterInit(() -> {
-                    if (isStashed) {
-                        mBarViewController.setExpanded(false);
-                    }
-                    mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
-                });
-            }
-        });
-        return animatorSet;
-    }
-
-    private float getStashTranslation() {
-        return (mUnstashedHeight - mStashedHeight) / 2f;
-    }
-
-    private void onIsStashedChanged() {
-        mControllers.runAfterInit(() -> {
-            mHandleViewController.onIsStashedChanged();
-            mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
-        });
-    }
-
-    public float getBubbleBarTranslationYForTaskbar() {
-        return -mActivity.getDeviceProfile().taskbarBottomMargin;
-    }
-
-    private float getBubbleBarTranslationYForHotseat() {
-        final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx;
-        final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx;
-        return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs(
-                hotseatCellHeight - mUnstashedHeight) / 2;
-    }
-
-    public float getBubbleBarTranslationY() {
-        // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
-        // Otherwise we're either showing in an app or in overview. In either case adjust it so
-        // the bubble bar aligns with the taskbar.
-        return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
-                : getBubbleBarTranslationYForTaskbar();
-    }
-
-    /**
-     * The difference on the Y axis between the center of the handle and the center of the bubble
-     * bar.
-     */
-    public float getDiffBetweenHandleAndBarCenters() {
-        // the difference between the centers of the handle and the bubble bar is the difference
-        // between their distance from the bottom of the screen.
-
-        float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f;
-        return mHandleCenterFromScreenBottom - barCenter;
-    }
-
-    /** The distance the handle moves as part of the new bubble animation. */
-    public float getStashedHandleTranslationForNewBubbleAnimation() {
-        // the should move up to the top of the stashed taskbar area. it is centered within it so
-        // it should move the same distance as it is away from the bottom.
-        return -mHandleCenterFromScreenBottom;
-    }
-
-    /** Checks whether the motion event is over the stash handle. */
-    public boolean isEventOverStashHandle(MotionEvent ev) {
-        return mHandleViewController.isEventOverHandle(ev);
-    }
-
-    /** Set a bubble bar location */
-    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
-        mHandleViewController.setBubbleBarLocation(bubbleBarLocation);
-    }
-
-    /** Returns the [PhysicsAnimator] for the stashed handle view. */
-    public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
-        return mHandleViewController.getPhysicsAnimator();
-    }
-
-    /** Notifies taskbar that it should update its touchable region. */
-    public void updateTaskbarTouchRegion() {
-        mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
-    }
-
-    /** Shows the bubble bar immediately without animation. */
-    public void showBubbleBarImmediate() {
-        mHandleViewController.setTranslationYForSwipe(0);
-        mIconTranslationYForStash.updateValue(getBubbleBarTranslationY());
-        mIconAlphaForStash.setValue(1);
-        mIconScaleForStash.updateValue(1);
-        mIsStashed = false;
-        onIsStashedChanged();
-    }
-
-    /** Stashes the bubble bar immediately without animation. */
-    public void stashBubbleBarImmediate() {
-        mHandleViewController.setTranslationYForSwipe(0);
-        mBubbleStashedHandleAlpha.setValue(1);
-        mIconAlphaForStash.setValue(0);
-        mIconTranslationYForStash.updateValue(getStashTranslation());
-        mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
-        mIsStashed = true;
-        onIsStashedChanged();
-    }
-
-    /**
-     * Updates the values of the internal animators after the new bubble animation was interrupted
-     *
-     * @param isStashed whether the current state should be stashed
-     * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
-     *                              bubble bar is showing to ensure that the stash animator runs
-     *                              smoothly.
-     */
-    public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) {
-        if (isStashed) {
-            mBubbleStashedHandleAlpha.setValue(1);
-            mIconAlphaForStash.setValue(0);
-            mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
-            mIconTranslationYForStash.updateValue(getStashTranslation());
-        } else {
-            mBubbleStashedHandleAlpha.setValue(0);
-            mHandleViewController.setTranslationYForSwipe(0);
-            mIconAlphaForStash.setValue(1);
-            mIconScaleForStash.updateValue(1);
-            mIconTranslationYForStash.updateValue(bubbleBarTranslationY);
-        }
-        mIsStashed = isStashed;
-        onIsStashedChanged();
-    }
-
-    /** Set the translation Y for the stashed handle. */
-    public void setHandleTranslationY(float ty) {
-        mHandleViewController.setTranslationYForSwipe(ty);
-    }
-
-    /** Dumps the state of BubbleStashController. */
-    public void dump(PrintWriter pw) {
-        pw.println("Bubble stash controller state:");
-        pw.println("  mIsStashed: " + mIsStashed);
-        pw.println("  mBubblesShowingOnOverview: " + mBubblesShowingOnOverview);
-        pw.println("  mBubblesShowingOnHome: " + mBubblesShowingOnHome);
-        pw.println("  mIsSysuiLocked: " + mIsSysuiLocked);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 91103d7..3640c3b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -21,6 +21,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.Nullable;
 import android.content.res.Resources;
 import android.graphics.Outline;
 import android.graphics.Rect;
@@ -28,18 +29,19 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.RevealOutlineAnimation;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.taskbar.StashedHandleView;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.handles.RegionSamplingHelper;
 
 /**
  * Handles properties/data collection, then passes the results to our stashed handle View to render.
@@ -49,23 +51,29 @@
     private final TaskbarActivityContext mActivity;
     private final StashedHandleView mStashedHandleView;
     private final MultiValueAlpha mStashedHandleAlpha;
+    private float mTranslationForSwipeY;
+    private float mTranslationForStashY;
 
     // Initialized in init.
     private BubbleBarViewController mBarViewController;
     private BubbleStashController mBubbleStashController;
     private RegionSamplingHelper mRegionSamplingHelper;
-    private int mBarSize;
-    private int mStashedTaskbarHeight;
+    // Height of the area for the stash handle. Handle will be drawn in the center of this.
+    // This is also the area where touch is handled on the handle.
+    private int mStashedBubbleBarHeight;
     private int mStashedHandleWidth;
     private int mStashedHandleHeight;
 
-    // The bounds we want to clip to in the settled state when showing the stashed handle.
+    // The bounds of the stashed handle in settled state.
     private final Rect mStashedHandleBounds = new Rect();
+    private float mStashedHandleRadius;
 
     // When the reveal animation is cancelled, we can assume it's about to create a new animation,
     // which should start off at the same point the cancelled one left off.
     private float mStartProgressForNextRevealAnim;
-    private boolean mWasLastRevealAnimReversed;
+    // Use a nullable boolean to handle initial case where the last animation direction is not known
+    @Nullable
+    private Boolean mWasLastRevealAnimReversed = null;
 
     // XXX: if there are more of these maybe do state flags instead
     private boolean mHiddenForSysui;
@@ -77,32 +85,39 @@
         mActivity = activity;
         mStashedHandleView = stashedHandleView;
         mStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1);
+        mStashedHandleAlpha.setUpdateVisibility(true);
     }
 
-    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+    /** Initialize controller. */
+    public void init(BubbleControllers bubbleControllers) {
         mBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleStashController = bubbleControllers.bubbleStashController;
 
+        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         Resources resources = mActivity.getResources();
         mStashedHandleHeight = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_handle_height);
         mStashedHandleWidth = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_handle_width);
-        mBarSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size);
 
-        final int bottomMargin = resources.getDimensionPixelSize(
-                R.dimen.transient_taskbar_bottom_margin);
-        mStashedHandleView.getLayoutParams().height = mBarSize + bottomMargin;
+        int barSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size);
+        // Use the max translation for bubble bar whether it is on the home screen or in app.
+        // Use values directly from device profile to avoid referencing other bubble controllers
+        // during init flow.
+        int maxTy = Math.max(deviceProfile.hotseatBarBottomSpacePx,
+                deviceProfile.taskbarBottomMargin);
+        // Adjust handle view size to accommodate the handle morphing into the bubble bar
+        mStashedHandleView.getLayoutParams().height = barSize + maxTy;
 
         mStashedHandleAlpha.get(0).setValue(0);
 
-        mStashedTaskbarHeight = resources.getDimensionPixelSize(
+        mStashedBubbleBarHeight = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_size);
         mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
             @Override
             public void getOutline(View view, Outline outline) {
-                float stashedHandleRadius = view.getHeight() / 2f;
-                outline.setRoundRect(mStashedHandleBounds, stashedHandleRadius);
+                mStashedHandleRadius = view.getHeight() / 2f;
+                outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
             }
         });
 
@@ -117,7 +132,7 @@
                     public Rect getSampledRegion(View sampledView) {
                         return mStashedHandleView.getSampledRegion();
                     }
-                }, Executors.UI_HELPER_EXECUTOR);
+                }, Executors.MAIN_EXECUTOR, Executors.UI_HELPER_EXECUTOR);
 
         mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
                 updateBounds(mBarViewController.getBubbleBarLocation()));
@@ -131,28 +146,25 @@
     private void updateBounds(BubbleBarLocation bubbleBarLocation) {
         // As more bubbles get added, the icon bounds become larger. To ensure a consistent
         // handle bar position, we pin it to the edge of the screen.
-        final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2;
+        final int stashedCenterY = mStashedHandleView.getHeight() - mStashedBubbleBarHeight / 2;
+        final int stashedCenterX;
         if (bubbleBarLocation.isOnLeft(mStashedHandleView.isLayoutRtl())) {
             final int left = mBarViewController.getHorizontalMargin();
-            mStashedHandleBounds.set(
-                    left,
-                    stashedCenterY - mStashedHandleHeight / 2,
-                    left + mStashedHandleWidth,
-                    stashedCenterY + mStashedHandleHeight / 2);
-            mStashedHandleView.setPivotX(0);
+            stashedCenterX = left + mStashedHandleWidth / 2;
         } else {
             final int right =
-                    mActivity.getDeviceProfile().widthPx - mBarViewController.getHorizontalMargin();
-            mStashedHandleBounds.set(
-                    right - mStashedHandleWidth,
-                    stashedCenterY - mStashedHandleHeight / 2,
-                    right,
-                    stashedCenterY + mStashedHandleHeight / 2);
-            mStashedHandleView.setPivotX(mStashedHandleView.getWidth());
+                    mStashedHandleView.getRight() - mBarViewController.getHorizontalMargin();
+            stashedCenterX = right - mStashedHandleWidth / 2;
         }
-
+        mStashedHandleBounds.set(
+                stashedCenterX - mStashedHandleWidth / 2,
+                stashedCenterY - mStashedHandleHeight / 2,
+                stashedCenterX + mStashedHandleWidth / 2,
+                stashedCenterY + mStashedHandleHeight / 2
+        );
         mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
-        mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f);
+        mStashedHandleView.setPivotX(stashedCenterX);
+        mStashedHandleView.setPivotY(stashedCenterY);
     }
 
     public void onDestroy() {
@@ -161,6 +173,13 @@
     }
 
     /**
+     * Returns the width of the stashed handle.
+     */
+    public int getStashedWidth() {
+        return mStashedHandleWidth;
+    }
+
+    /**
      * Returns the height of the stashed handle.
      */
     public int getStashedHeight() {
@@ -168,10 +187,10 @@
     }
 
     /**
-     * Returns the height when the bubble bar is unstashed (so the height of the bubble bar).
+     * Returns bounds of the stashed handle view
      */
-    public int getUnstashedHeight() {
-        return mBarSize;
+    public void getBounds(Rect bounds) {
+        bounds.set(mStashedHandleBounds);
     }
 
     /**
@@ -241,7 +260,25 @@
      * Sets the translation of the stashed handle during the swipe up gesture.
      */
     public void setTranslationYForSwipe(float transY) {
-        mStashedHandleView.setTranslationY(transY);
+        mTranslationForSwipeY = transY;
+        updateTranslationY();
+    }
+
+    /**
+     * Sets the translation of the stashed handle during the spring on stash animation.
+     */
+    public void setTranslationYForStash(float transY) {
+        mTranslationForStashY = transY;
+        updateTranslationY();
+    }
+
+    private void updateTranslationY() {
+        mStashedHandleView.setTranslationY(mTranslationForSwipeY + mTranslationForStashY);
+    }
+
+    /** Returns the translation of the stashed handle. */
+    public float getTranslationY() {
+        return mStashedHandleView.getTranslationY();
     }
 
     /**
@@ -257,18 +294,17 @@
      * the size of where the bubble bar icons will be.
      */
     public Animator createRevealAnimToIsStashed(boolean isStashed) {
-        Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
+        Rect bubbleBarBounds = getLocalBubbleBarBounds();
 
-        // Account for the full visual height of the bubble bar
-        int heightDiff = (mBarSize - bubbleBarBounds.height()) / 2;
-        bubbleBarBounds.top -= heightDiff;
-        bubbleBarBounds.bottom += heightDiff;
-        float stashedHandleRadius = mStashedHandleView.getHeight() / 2f;
+        float bubbleBarRadius = bubbleBarBounds.height() / 2f;
         final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
-                stashedHandleRadius, stashedHandleRadius, bubbleBarBounds, mStashedHandleBounds);
+                bubbleBarRadius, mStashedHandleRadius, bubbleBarBounds, mStashedHandleBounds);
 
         boolean isReversed = !isStashed;
-        boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
+        // We are only changing direction when mWasLastRevealAnimReversed is set at least once
+        boolean changingDirection =
+                mWasLastRevealAnimReversed != null && mWasLastRevealAnimReversed != isReversed;
+
         mWasLastRevealAnimReversed = isReversed;
         if (changingDirection) {
             mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim;
@@ -285,6 +321,21 @@
         return revealAnim;
     }
 
+    /**
+     * Get bounds for the bubble bar in the space of the handle view
+     */
+    private Rect getLocalBubbleBarBounds() {
+        // Position the bubble bar bounds to the space of handle view
+        Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
+        // Start by moving bubble bar bounds to the bottom of handle view
+        int height = bubbleBarBounds.height();
+        bubbleBarBounds.bottom = mStashedHandleView.getHeight();
+        bubbleBarBounds.top = bubbleBarBounds.bottom - height;
+        // Then apply translation that is applied to the bubble bar
+        bubbleBarBounds.offset(0, (int) mBubbleStashController.getBubbleBarTranslationY());
+        return bubbleBarBounds;
+    }
+
     /** Checks that the stash handle is visible and that the motion event is within bounds. */
     public boolean isEventOverHandle(MotionEvent ev) {
         if (mStashedHandleView.getVisibility() != VISIBLE) {
@@ -292,15 +343,11 @@
         }
 
         // the bounds of the handle only include the visible part, so we check that the Y coordinate
-        // is anywhere within the stashed taskbar height.
-        int top = mActivity.getDeviceProfile().heightPx - mStashedTaskbarHeight;
-
-        return (int) ev.getRawY() >= top && containsX((int) ev.getRawX());
-    }
-
-    /** Checks if the given x coordinate is within the stashed handle bounds. */
-    public boolean containsX(int x) {
-        return x >= mStashedHandleBounds.left && x <= mStashedHandleBounds.right;
+        // is anywhere within the stashed height of bubble bar (same as taskbar stashed height).
+        final int top = mActivity.getDeviceProfile().heightPx - mStashedBubbleBarHeight;
+        final float x = ev.getRawX();
+        return ev.getRawY() >= top && x >= mStashedHandleBounds.left
+                && x <= mStashedHandleBounds.right;
     }
 
     /** Set a bubble bar location */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 4c468bb..c74fa9b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -16,25 +16,29 @@
 package com.android.launcher3.taskbar.bubbles;
 
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Outline;
+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;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
 import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.icons.IconNormalizer;
-import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleInfo;
 
 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
 
@@ -45,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;
@@ -64,12 +70,22 @@
     private float mAnimatingToDotScale;
     // The current scale value of the dot
     private float mDotScale;
+    private boolean mDotSuppressedForBubbleUpdate = false;
 
     // TODO: (b/273310265) handle RTL
     // Whether the bubbles are positioned on the left or right side of the screen
     private boolean mOnLeft = false;
 
     private BubbleBarItem mBubble;
+    private boolean mIsOverflow;
+
+    private Bitmap mIcon;
+
+    @Nullable
+    private Controller mController;
+
+    @Nullable
+    private BubbleBarBubbleIconsFactory mIconFactory = null;
 
     public BubbleView(Context context) {
         this(context, null);
@@ -97,25 +113,18 @@
 
         setFocusable(true);
         setClickable(true);
-        setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                BubbleView.this.getOutline(outline);
-            }
-        });
-    }
 
-    private void getOutline(Outline outline) {
-        updateBubbleSizeAndDotRender();
-        final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
-        final int inset = (mBubbleSize - normalizedSize) / 2;
-        outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+        // We manage the shadow ourselves when creating the bitmap
+        setOutlineAmbientShadowColor(Color.TRANSPARENT);
+        setOutlineSpotShadowColor(Color.TRANSPARENT);
     }
 
     private void updateBubbleSizeAndDotRender() {
         int updatedBubbleSize = Math.min(getWidth(), getHeight());
         if (updatedBubbleSize == mBubbleSize) return;
         mBubbleSize = updatedBubbleSize;
+        mIconFactory = new BubbleBarBubbleIconsFactory(mContext, mBubbleSize);
+        updateBubbleIcon();
         if (mBubble == null || mBubble instanceof BubbleBarOverflow) return;
         Path dotPath = ((BubbleBarBubble) mBubble).getDotPath();
         mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE);
@@ -150,16 +159,16 @@
         applyDragTranslation();
     }
 
+    private void applyDragTranslation() {
+        setTranslationX(mDragTranslationX + mOffsetX);
+    }
+
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         updateBubbleSizeAndDotRender();
     }
 
-    private void applyDragTranslation() {
-        setTranslationX(mDragTranslationX + mOffsetX);
-    }
-
     @Override
     public void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
@@ -178,11 +187,70 @@
         mDotRenderer.draw(canvas, mDrawParams);
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+        if (mBubble instanceof BubbleBarBubble) {
+            info.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
+        }
+        if (mController != null) {
+            if (mController.getBubbleBarLocation().isOnLeft(isLayoutRtl())) {
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+                        getResources().getString(R.string.bubble_bar_action_move_right)));
+            } else {
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+                        getResources().getString(R.string.bubble_bar_action_move_left)));
+            }
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
+            if (mController != null) {
+                mController.collapse();
+            }
+            return true;
+        }
+        if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
+            if (mController != null) {
+                mController.dismiss(this);
+            }
+            return true;
+        }
+        if (action == R.id.action_move_left) {
+            if (mController != null) {
+                mController.updateBubbleBarLocation(BubbleBarLocation.LEFT,
+                        BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE);
+            }
+        }
+        if (action == R.id.action_move_right) {
+            if (mController != null) {
+                mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT,
+                        BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE);
+            }
+        }
+        return false;
+    }
+
+    void setController(@Nullable Controller controller) {
+        mController = controller;
+    }
+
     /** Sets the bubble being rendered in this view. */
     public void setBubble(BubbleBarBubble bubble) {
         mBubble = bubble;
-        mBubbleIcon.setImageBitmap(bubble.getIcon());
-        mAppIcon.setImageBitmap(bubble.getBadge());
+        mIcon = bubble.getIcon();
+        updateBubbleIcon();
+        if (bubble.getInfo().showAppBadge()) {
+            mAppIcon.setImageBitmap(bubble.getBadge());
+        } else {
+            mAppIcon.setVisibility(GONE);
+        }
         mDotColor = bubble.getDotColor();
         mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE);
         String contentDesc = bubble.getInfo().getTitle();
@@ -197,6 +265,18 @@
         setContentDescription(contentDesc);
     }
 
+    private void updateBubbleIcon() {
+        Bitmap icon = null;
+        if (mIcon != null) {
+            icon = mIcon;
+            if (mIconFactory != null) {
+                BitmapDrawable iconDrawable = new BitmapDrawable(getResources(), icon);
+                icon = mIconFactory.createShadowedIconBitmap(iconDrawable, /* scale = */ 1f);
+            }
+        }
+        mBubbleIcon.setImageBitmap(icon);
+    }
+
     /**
      * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
      * but does not represent app content, instead it shows recent bubbles that couldn't fit into
@@ -205,21 +285,33 @@
      */
     public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
         mBubble = overflow;
-        mBubbleIcon.setImageBitmap(bitmap);
+        mIsOverflow = true;
+        mIcon = bitmap;
+        updateBubbleIcon();
         mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
         setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description));
     }
 
+    /** Whether this view represents the overflow button. */
+    public boolean isOverflow() {
+        return mIsOverflow;
+    }
+
     /** Returns the bubble being rendered in this view. */
     @Nullable
     public BubbleBarItem getBubble() {
         return mBubble;
     }
 
-    void updateDotVisibility(boolean animate) {
-        final float targetScale = shouldDrawDot() ? 1f : 0f;
+    /** 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();
+            animateDotScale(targetScale);
         } else {
             mDotScale = targetScale;
             mAnimatingToDotScale = targetScale;
@@ -227,32 +319,69 @@
         }
     }
 
-    void updateBadgeVisibility(boolean show) {
-        if (mBubble instanceof BubbleBarOverflow) {
-            // The overflow bubble does not have a badge, so just bail.
+    void setBadgeScale(float fraction) {
+        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;
         }
-        BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
-        Bitmap appBadgeBitmap = bubble.getBadge();
-        int translationX = mOnLeft
-                ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
-                : 0;
-        mAppIcon.setTranslationX(translationX);
-        mAppIcon.setVisibility(show ? VISIBLE : GONE);
+        mAppIcon.clearAnimation();
+        mAppIcon.animate()
+                .setDuration(SCALE_ANIMATION_DURATION_MS)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .scaleX(scale)
+                .scaleY(scale)
+                .start();
     }
 
-    /** Whether the dot indicating unseen content in a bubble should be shown. */
-    private boolean shouldDrawDot() {
-        boolean bubbleHasUnseenContent = mBubble != null
+    /** Suppresses or un-suppresses drawing the dot due to an update for this bubble. */
+    public void suppressDotForBubbleUpdate(boolean suppress) {
+        mDotSuppressedForBubbleUpdate = suppress;
+        if (suppress) {
+            setDotScale(0);
+        } else {
+            showDotIfNeeded(/* animate= */ false);
+        }
+    }
+
+    boolean hasUnseenContent() {
+        return mBubble != null
                 && mBubble instanceof BubbleBarBubble
                 && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
-        // Always render the dot if it's animating, since it could be animating out. Otherwise, show
-        // it if the bubble wants to show it, and we aren't suppressing it.
-        return bubbleHasUnseenContent || mDotIsAnimating;
     }
 
-    /** How big the dot should be, fraction from 0 to 1. */
-    void setDotScale(float fraction) {
+    /**
+     * Used to determine if we can skip drawing frames.
+     *
+     * <p>Generally we should draw the dot when it is requested to be shown and there is unseen
+     * content. But when the dot is removed, we still want to draw frames so that it can be scaled
+     * out.
+     */
+    private boolean shouldDrawDot() {
+        // if there's no dot there's nothing to draw, unless the dot was removed and we're in the
+        // middle of removing it
+        return hasUnseenContent() || mDotIsAnimating;
+    }
+
+    /** Updates the dot scale to the specified fraction from 0 to 1. */
+    private void setDotScale(float fraction) {
         if (!shouldDrawDot()) {
             return;
         }
@@ -260,11 +389,41 @@
         invalidate();
     }
 
-    /**
-     * Animates the dot to the given scale.
-     */
-    private void animateDotScale() {
-        float toScale = shouldDrawDot() ? 1f : 0f;
+    void showDotIfNeeded(float fraction) {
+        if (!hasUnseenContent()) {
+            return;
+        }
+        setDotScale(fraction);
+    }
+
+    void showDotIfNeeded(boolean animate) {
+        // only show the dot if we have unseen content and it's not suppressed
+        if (!hasUnseenContent() || mDotSuppressedForBubbleUpdate) {
+            return;
+        }
+        if (animate) {
+            animateDotScale(1f);
+        } else {
+            setDotScale(1f);
+        }
+    }
+
+    void hideDot() {
+        animateDotScale(0f);
+    }
+
+    /** Marks this bubble such that it no longer has unseen content, and hides the dot. */
+    void markSeen() {
+        if (mBubble instanceof BubbleBarBubble bubble) {
+            BubbleInfo info = bubble.getInfo();
+            info.setFlags(
+                    info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+            hideDot();
+        }
+    }
+
+    /** Animates the dot to the given scale. */
+    private void animateDotScale(float toScale) {
         boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0;
 
         // Don't restart the animation if we're already animating to the given value or if the dot
@@ -277,11 +436,9 @@
 
         final boolean showDot = toScale > 0f;
 
-        // Do NOT wait until after animation ends to setShowDot
-        // to avoid overriding more recent showDot states.
         clearAnimation();
         animate()
-                .setDuration(200)
+                .setDuration(SCALE_ANIMATION_DURATION_MS)
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .setUpdateListener((valueAnimator) -> {
                     float fraction = valueAnimator.getAnimatedFraction();
@@ -293,10 +450,42 @@
                 }).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";
         return "BubbleView{" + toString + "}";
     }
+
+    /** Interface for BubbleView to communicate with its controller */
+    public interface Controller {
+        /** Get current bubble bar {@link BubbleBarLocation} */
+        BubbleBarLocation getBubbleBarLocation();
+
+        /** This bubble should be dismissed */
+        void dismiss(BubbleView bubble);
+
+        /** Collapse the bubble bar */
+        void collapse();
+
+        /** Request bubble bar location to be updated to the given 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 feff9fd..6c354f3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -26,8 +26,10 @@
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
-import com.android.launcher3.taskbar.bubbles.BubbleStashController
 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
 
 /** Handles animations for bubble bar bubbles. */
@@ -36,16 +38,28 @@
 constructor(
     private val bubbleBarView: BubbleBarView,
     private val bubbleStashController: BubbleStashController,
-    private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
+    private val bubbleBarFlyoutController: BubbleBarFlyoutController,
+    private val onExpanded: Runnable,
+    private val scheduler: Scheduler = HandlerScheduler(bubbleBarView),
 ) {
 
     private var animatingBubble: AnimatingBubble? = null
     private val bubbleBarBounceDistanceInPx =
         bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
 
+    fun hasAnimation() = animatingBubble != null
+
+    val isAnimating: Boolean
+        get() {
+            val animatingBubble = animatingBubble ?: return false
+            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. */
@@ -58,8 +72,33 @@
     private data class AnimatingBubble(
         val bubbleView: BubbleView,
         val showAnimation: Runnable,
-        val hideAnimation: Runnable
-    )
+        val hideAnimation: Runnable,
+        val expand: Boolean,
+        val state: State = State.CREATED,
+    ) {
+
+        /**
+         * The state of the animation.
+         *
+         * The animation is initially created but will be scheduled later using the [Scheduler].
+         *
+         * The normal uninterrupted cycle is for the bubble notification to animate in, then be in a
+         * transient state and eventually to animate out.
+         *
+         * However different events, such as touch and external signals, may cause the animation to
+         * end earlier.
+         */
+        enum class State {
+            /** The animation is created but not started yet. */
+            CREATED,
+            /** The bubble notification is animating in. */
+            ANIMATING_IN,
+            /** The bubble notification is now fully showing and waiting to be hidden. */
+            IN,
+            /** The bubble notification is animating out. */
+            ANIMATING_OUT,
+        }
+    }
 
     /** An interface for scheduling jobs. */
     interface Scheduler {
@@ -93,19 +132,34 @@
     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) {
+    fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
+        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 = buildBubbleBarToHandleAnimation()
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation()
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
@@ -124,40 +178,47 @@
      * 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 {
-        // prepare the bubble bar for the animation
-        bubbleBarView.onAnimatingBubbleStarted()
-        bubbleBarView.visibility = VISIBLE
-        bubbleBarView.alpha = 0f
-        bubbleBarView.translationY = 0f
-        bubbleBarView.scaleX = 1f
-        bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
-        bubbleBarView.relativePivotY = 0.5f
+    private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable {
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
+        // 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,
         // the translation y is offset by this value to make the transition from the handle to the
         // bar smooth.
-        val offset = bubbleStashController.diffBetweenHandleAndBarCenters
+        val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
+        val stashedHandleTranslationYForAnimation =
+            bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
         val stashedHandleTranslationY =
-            bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
+            bubbleStashController.getHandleTranslationY() ?: return@Runnable
+        val translationTracker = TranslationTracker(stashedHandleTranslationY)
 
         // this is the total distance that both the stashed handle and the bubble will be traveling
         // at the end of the animation the bubble bar will be positioned in the same place when it
         // shows while we're in an app.
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
-        val animator = bubbleStashController.stashedHandlePhysicsAnimator
+        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 {
-                ty >= stashedHandleTranslationY -> {
+                ty >= stashedHandleTranslationYForAnimation -> {
                     // we're in the first leg of the animation. only animate the handle. the bubble
                     // bar remains hidden during this part of the animation
 
                     // map the path [0, stashedHandleTranslationY] to [0,1]
-                    val fraction = ty / stashedHandleTranslationY
+                    val fraction = ty / stashedHandleTranslationYForAnimation
                     handle.alpha = 1 - fraction
                 }
                 ty >= totalTranslationY -> {
@@ -171,8 +232,8 @@
                     if (bubbleBarView.alpha != 1f) {
                         // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1]
                         val fraction =
-                            (ty - stashedHandleTranslationY) /
-                                (totalTranslationY - stashedHandleTranslationY)
+                            (ty - stashedHandleTranslationYForAnimation) /
+                                (totalTranslationY - stashedHandleTranslationYForAnimation)
                         bubbleBarView.alpha = fraction
                         bubbleBarView.scaleY =
                             BUBBLE_ANIMATION_INITIAL_SCALE_Y +
@@ -192,18 +253,17 @@
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
             }
+            translationTracker.updateTyAndExpandIfNeeded(ty)
         }
         animator.addEndListener { _, _, _, canceled, _, _, _ ->
             // if the show animation was canceled, also cancel the hide animation. this is typically
             // canceled in this class, but could potentially be canceled elsewhere.
-            if (canceled) {
-                val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
-                scheduler.cancel(hideAnimation)
-                animatingBubble = null
-                bubbleBarView.onAnimatingBubbleCompleted()
-                bubbleBarView.relativePivotY = 1f
+            if (canceled || animatingBubble?.expand == true) {
+                cancelHideAnimation()
                 return@addEndListener
             }
+            setupAndShowFlyout()
+
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -226,13 +286,14 @@
      */
     private fun buildBubbleBarToHandleAnimation() = Runnable {
         if (animatingBubble == null) return@Runnable
-        val offset = bubbleStashController.diffBetweenHandleAndBarCenters
+        moveToState(AnimatingBubble.State.ANIMATING_OUT)
+        val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
         val stashedHandleTranslationY =
-            bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
+            bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
         // this is the total distance that both the stashed handle and the bar will be traveling
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
         bubbleStashController.setHandleTranslationY(totalTranslationY)
-        val animator = bubbleStashController.stashedHandlePhysicsAnimator
+        val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
         animator.addUpdateListener { handle, values ->
@@ -268,14 +329,36 @@
                 }
             }
         }
-        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.onAnimatingBubbleCompleted()
             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. */
@@ -283,59 +366,84 @@
         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()
-                    bubbleBarView.onAnimatingBubbleCompleted()
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
             }
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
     private fun buildBubbleBarSpringInAnimation() = Runnable {
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
         // prepare the bubble bar for the animation
-        bubbleBarView.onAnimatingBubbleStarted()
         bubbleBarView.translationY = bubbleBarView.height.toFloat()
         bubbleBarView.visibility = VISIBLE
         bubbleBarView.alpha = 1f
         bubbleBarView.scaleX = 1f
         bubbleBarView.scaleY = 1f
 
+        val translationTracker = TranslationTracker(bubbleBarView.translationY)
+
         val animator = PhysicsAnimator.getInstance(bubbleBarView)
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
-        animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() }
+        animator.addUpdateListener { _, values ->
+            val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
+            translationTracker.updateTyAndExpandIfNeeded(ty)
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
         animator.addEndListener { _, _, _, _, _, _, _ ->
+            if (animatingBubble?.expand == true) {
+                cancelHideAnimation()
+            } else {
+                setupAndShowFlyout()
+            }
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
         animator.start()
     }
 
-    fun animateBubbleBarForCollapsed(b: BubbleBarBubble) {
+    fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
+        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()
-            bubbleBarView.onAnimatingBubbleCompleted()
             bubbleStashController.updateTaskbarTouchRegion()
         }
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
@@ -346,47 +454,215 @@
      * the bubble bar moves back to its initial position with a spring animation.
      */
     private fun buildBubbleBarBounceAnimation() = Runnable {
-        bubbleBarView.onAnimatingBubbleStarted()
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
         val ty = bubbleStashController.bubbleBarTranslationY
 
         val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
         springBackAnimation.setDefaultSpringConfig(springConfig)
         springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
+        springBackAnimation.addEndListener { _, _, _, _, _, _, _ ->
+            if (animatingBubble?.expand == true) {
+                expandBubbleBar()
+                cancelHideAnimation()
+            } else {
+                setupAndShowFlyout()
+            }
+        }
 
         // animate the bubble bar up and start the spring back down animation when it ends.
         ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
             .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
-            .withEndAction { springBackAnimation.start() }
+            .withEndAction {
+                if (animatingBubble?.expand == true) expandBubbleBar()
+                springBackAnimation.start()
+            }
             .start()
     }
 
-    /** Handles touching the animating bubble bar. */
-    fun onBubbleBarTouchedWhileAnimating() {
+    private fun setupAndShowFlyout() {
+        val bubbleView = animatingBubble?.bubbleView
+        val bubble = bubbleView?.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            bubbleBarFlyoutController.setUpAndShowFlyout(
+                BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message),
+                onInit = { bubbleView.suppressDotForBubbleUpdate(true) },
+                onEnd = {
+                    moveToState(AnimatingBubble.State.IN)
+                    bubbleStashController.updateTaskbarTouchRegion()
+                },
+            )
+        } else {
+            moveToState(AnimatingBubble.State.IN)
+        }
+    }
+
+    private fun cancelFlyout() {
+        bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved() }
+    }
+
+    private fun onFlyoutRemoved() {
+        animatingBubble?.bubbleView?.suppressDotForBubbleUpdate(false)
+        bubbleStashController.updateTaskbarTouchRegion()
+    }
+
+    /** Interrupts the animation due to touching the bubble bar or flyout. */
+    fun interruptForTouch() {
         PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
-        bubbleStashController.stashedHandlePhysicsAnimator.cancelIfRunning()
+        bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
+        cancelFlyout()
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
-        bubbleBarView.onAnimatingBubbleCompleted()
         bubbleBarView.relativePivotY = 1f
         animatingBubble = null
     }
 
     /** 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
-        bubbleStashController.stashedHandlePhysicsAnimator.cancel()
-        bubbleBarView.onAnimatingBubbleCompleted()
+        bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
         bubbleBarView.relativePivotY = 1f
         bubbleStashController.onNewBubbleAnimationInterrupted(
             /* isStashed= */ bubbleBarView.alpha == 0f,
-            bubbleBarView.translationY
+            bubbleBarView.translationY,
         )
     }
 
-    private fun <T> PhysicsAnimator<T>.cancelIfRunning() {
-        if (isRunning()) cancel()
+    fun expandedWhileAnimating() {
+        val animatingBubble = animatingBubble ?: return
+        this.animatingBubble = animatingBubble.copy(expand = true)
+        // if we're fully in and waiting to hide, cancel the hide animation and clean up
+        if (animatingBubble.state == AnimatingBubble.State.IN) {
+            cancelFlyout()
+            expandBubbleBar()
+            cancelHideAnimation()
+        }
+    }
+
+    private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) {
+        val animatingBubble = animatingBubble ?: return
+        when (animatingBubble.state) {
+            AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started
+            AnimatingBubble.State.ANIMATING_IN ->
+                updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding)
+            AnimatingBubble.State.IN ->
+                updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding)
+            AnimatingBubble.State.ANIMATING_OUT ->
+                updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding)
+        }
+    }
+
+    private fun updateAnimationWhileAnimatingIn(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+        if (!bubbleBarFlyoutController.hasFlyout()) {
+            // if the flyout does not yet exist, then we're only animating the bubble bar.
+            // the animating bubble has been updated, so the when the flyout expands it will
+            // show the right message. we only need to update the dot visibility.
+            bubbleView.updateDotVisibility(/* animate= */ !bubbleStashController.isStashed)
+            return
+        }
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            // the flyout is currently expanding and we need to update it with new data
+            bubbleView.suppressDotForBubbleUpdate(true)
+            bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout)
+        } else {
+            // the flyout is expanding but we don't have new flyout data to update it with,
+            // so cancel the expanding flyout.
+            cancelFlyout()
+        }
+    }
+
+    private fun updateAnimationWhileIn(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        // unsuppress the current bubble because we are about to hide its flyout
+        animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+        // we're currently idle, waiting for the hide animation to start. update the flyout
+        // data and reschedule the hide animation to run later to give the user a chance to
+        // see the new flyout.
+        val hideAnimation = animatingBubble.hideAnimation
+        scheduler.cancel(hideAnimation)
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            bubbleView.suppressDotForBubbleUpdate(true)
+            bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) {
+                bubbleStashController.updateTaskbarTouchRegion()
+            }
+        } else {
+            cancelFlyout()
+        }
+    }
+
+    private fun updateAnimationWhileAnimatingOut(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        // unsuppress the current bubble because we are about to hide its flyout
+        animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+        // the hide animation already started so it can't be canceled, just post it again
+        val hideAnimation = animatingBubble.hideAnimation
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (bubbleBarFlyoutController.hasFlyout()) {
+            // the flyout is collapsing. update it with the new flyout
+            if (flyout != null) {
+                moveToState(AnimatingBubble.State.ANIMATING_IN)
+                bubbleView.suppressDotForBubbleUpdate(true)
+                bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) {
+                    moveToState(AnimatingBubble.State.IN)
+                    bubbleStashController.updateTaskbarTouchRegion()
+                }
+            } else {
+                cancelFlyout()
+                moveToState(AnimatingBubble.State.IN)
+            }
+        } else {
+            // the flyout is already gone. if we're animating the handle cancel it. the
+            // animation itself can handle morphing back into the bubble bar and restarting
+            // and show the flyout.
+            val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator()
+            if (handleAnimator != null && handleAnimator.isRunning()) {
+                interceptedHandleAnimator = true
+                handleAnimator.cancel()
+            }
+
+            // if we're not animating the handle, then the hide animation simply hides the
+            // flyout, but if the flyout is gone then the animation has ended.
+        }
+    }
+
+    private fun cancelHideAnimation() {
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        animatingBubble = null
+        bubbleBarView.relativePivotY = 1f
+        bubbleStashController.showBubbleBarImmediate()
+    }
+
+    private fun <T> PhysicsAnimator<T>?.cancelIfRunning() {
+        if (this?.isRunning() == true) cancel()
     }
 
     private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator {
@@ -404,4 +680,42 @@
         )
         return this
     }
+
+    private fun moveToState(state: AnimatingBubble.State) {
+        val animatingBubble = this.animatingBubble ?: return
+        this.animatingBubble = animatingBubble.copy(state = state)
+    }
+
+    private fun expandBubbleBar() {
+        bubbleBarView.isExpanded = true
+        onExpanded.run()
+    }
+
+    /**
+     * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands
+     * as part of the animation, the expansion should start after the bubble bar reaches the peak
+     * position.
+     */
+    private inner class TranslationTracker(initialTy: Float) {
+        private var previousTy = initialTy
+        private var startedExpanding = false
+        private var reachedPeak = false
+
+        fun updateTyAndExpandIfNeeded(ty: Float) {
+            if (!reachedPeak) {
+                // the bubble bar is positioned at the bottom of the screen and moves up using
+                // negative ty values. the peak is reached the first time we see a value that is
+                // greater than the previous.
+                if (ty > previousTy) {
+                    reachedPeak = true
+                }
+            }
+            val expand = animatingBubble?.expand ?: false
+            if (reachedPeak && expand && !startedExpanding) {
+                expandBubbleBar()
+                startedExpanding = true
+            }
+            previousTy = ty
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
new file mode 100644
index 0000000..7b20eea
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.graphics.Rect
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.core.animation.ValueAnimator
+import com.android.launcher3.R
+import com.android.systemui.util.addListener
+
+/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
+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 ANIMATION_DURATION_MS = 250L
+    }
+
+    private var flyout: BubbleBarFlyoutView? = null
+    private var animator: ValueAnimator? = null
+    private val horizontalMargin =
+        container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
+
+    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, positioner, flyoutScheduler)
+
+        flyout.translationY = positioner.targetTy
+
+        val lp =
+            FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                Gravity.BOTTOM or if (positioner.isOnLeft) Gravity.LEFT else Gravity.RIGHT,
+            )
+        lp.marginStart = horizontalMargin
+        lp.marginEnd = horizontalMargin
+        container.addView(flyout, lp)
+
+        this.flyout = flyout
+        flyout.showFromCollapsed(message) {
+            flyout.updateExpansionProgress(0f)
+            onInit()
+            showFlyout(AnimationType.MORPH, onEnd)
+        }
+    }
+
+    private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
+        val flyout = this.flyout ?: return
+        val startValue = getCurrentAnimatedValueIfRunning() ?: 0f
+        val duration = (ANIMATION_DURATION_MS * (1f - startValue)).toLong()
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration)
+        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) {
+            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 = (ANIMATION_DURATION_MS * startValue).toLong()
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration)
+        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@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
new file mode 100644
index 0000000..14b456c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.graphics.drawable.Drawable
+
+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
new file mode 100644
index 0000000..aa2555e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.graphics.PointF
+
+/** Provides positioning data to the flyout view. */
+interface BubbleBarFlyoutPositioner {
+
+    /** Whether the flyout view should be positioned on left or the right edge. */
+    val isOnLeft: Boolean
+
+    /** 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
new file mode 100644
index 0000000..418675c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -0,0 +1,406 @@
+/*
+ * 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.content.Context
+import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Outline
+import android.graphics.Paint
+import android.graphics.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
+
+/** The flyout view used to notify the user of a new bubble notification. */
+class BubbleBarFlyoutView(
+    context: Context,
+    private val positioner: BubbleBarFlyoutPositioner,
+    scheduler: FlyoutScheduler? = null,
+) : ConstraintLayout(context) {
+
+    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
+    }
+
+    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) }
+
+    private val flyoutPadding by
+        lazy(LazyThreadSafetyMode.NONE) {
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding)
+        }
+
+    private val triangleHeight by
+        lazy(LazyThreadSafetyMode.NONE) {
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_height)
+        }
+
+    private val triangleOverlap by
+        lazy(LazyThreadSafetyMode.NONE) {
+            context.resources.getDimensionPixelSize(
+                R.dimen.bubblebar_flyout_triangle_overlap_amount
+            )
+        }
+
+    private val triangleWidth by
+        lazy(LazyThreadSafetyMode.NONE) {
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_width)
+        }
+
+    private val triangleRadius by
+        lazy(LazyThreadSafetyMode.NONE) {
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_radius)
+        }
+
+    private val minFlyoutWidth by
+        lazy(LazyThreadSafetyMode.NONE) {
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_min_width)
+        }
+
+    private val maxFlyoutWidth by
+        lazy(LazyThreadSafetyMode.NONE) {
+            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
+     * tinted notification dot.
+     */
+    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 = 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 = flyoutElevation
+
+        RoundedArrowDrawable.addDownPointingRoundedTriangleToPath(
+            triangleWidth.toFloat(),
+            triangleHeight.toFloat(),
+            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)
+    }
+
+    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
+
+        // 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 {
+            icon.visibility = GONE
+        }
+
+        val minTextViewWidth: Int
+        val maxTextViewWidth: Int
+        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
+            minTextViewWidth = minFlyoutWidth - flyoutPadding * 2
+            maxTextViewWidth = minTextViewWidth
+        }
+
+        if (flyoutMessage.title.isEmpty()) {
+            title.visibility = GONE
+        } else {
+            title.minWidth = minTextViewWidth
+            title.maxWidth = maxTextViewWidth
+            title.text = flyoutMessage.title
+            title.visibility = VISIBLE
+        }
+
+        message.minWidth = minTextViewWidth
+        message.maxWidth = maxTextViewWidth
+        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) {
+        // 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,
+        )
+
+        backgroundPaint.color =
+            ArgbEvaluator.getInstance().evaluate(expansionProgress, 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,
+        )
+        if (expansionProgress >= minExpansionProgressForTriangle) {
+            drawTriangle(canvas)
+        }
+        canvas.restore()
+        invalidateOutline()
+        super.onDraw(canvas)
+    }
+
+    private fun drawTriangle(canvas: Canvas) {
+        canvas.save()
+        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
+        val defaultBackgroundColor = if (isNightModeOn) Color.BLACK else Color.WHITE
+        val defaultTextColor = if (isNightModeOn) Color.WHITE else Color.BLACK
+        val ta =
+            context.obtainStyledAttributes(
+                intArrayOf(
+                    com.android.internal.R.attr.materialColorSurfaceContainer,
+                    com.android.internal.R.attr.materialColorOnSurface,
+                    com.android.internal.R.attr.materialColorOnSurfaceVariant,
+                )
+            )
+        backgroundColor = ta.getColor(0, defaultBackgroundColor)
+        title.setTextColor(ta.getColor(1, defaultTextColor))
+        message.setTextColor(ta.getColor(2, defaultTextColor))
+        ta.recycle()
+        backgroundPaint.color = backgroundColor
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
new file mode 100644
index 0000000..e2f010a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
@@ -0,0 +1,29 @@
+/*
+ * 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
+
+/** 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)
+
+    /** 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
new file mode 100644
index 0000000..d9589bb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.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.taskbar.bubbles.stashing
+
+import android.graphics.Rect
+import android.view.InsetsController
+import android.view.MotionEvent
+import android.view.View
+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.BubbleStashedHandleViewController
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import java.io.PrintWriter
+
+/** StashController that defines stashing behaviour for the taskbar modes. */
+interface BubbleStashController {
+
+    /**
+     * Abstraction on the task bar activity context to only provide the dimensions required for
+     * [BubbleBarView] translation Y computation.
+     */
+    interface TaskbarHotseatDimensionsProvider {
+
+        /** Provides taskbar bottom space in pixels. */
+        fun getTaskbarBottomSpace(): Int
+
+        /** Provides taskbar height in pixels. */
+        fun getTaskbarHeight(): Int
+    }
+
+    /** Execute passed action only after controllers are initiated. */
+    interface ControllersAfterInitAction {
+        /** Execute action after controllers are initiated. */
+        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. */
+    val isBubblesShowingOnHome: Boolean
+        get() = launcherState == BubbleLauncherState.HOME
+
+    /** Whether launcher enters or exits the overview page. */
+    val isBubblesShowingOnOverview: Boolean
+        get() = launcherState == BubbleLauncherState.OVERVIEW
+
+    /** Updated when sysui locked state changes, when locked, bubble bar is not shown. */
+    var isSysuiLocked: Boolean
+
+    /** Whether there is a transient taskbar mode */
+    val isTransientTaskBar: Boolean
+
+    /** Whether stash control has a handle view */
+    val hasHandleView: Boolean
+
+    /** Initialize controller */
+    fun init(
+        taskbarInsetsController: TaskbarInsetsController,
+        bubbleBarViewController: BubbleBarViewController,
+        bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+        controllersAfterInitAction: ControllersAfterInitAction,
+    )
+
+    /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */
+    fun showBubbleBarImmediate()
+
+    /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */
+    fun showBubbleBarImmediate(bubbleBarTranslationY: Float)
+
+    /** Stashes the bubble bar immediately without animation. */
+    fun stashBubbleBarImmediate()
+
+    /** Returns the touchable height of the bubble bar based on it's stashed state. */
+    fun getTouchableHeight(): Int
+
+    /** Whether bubble bar is currently visible */
+    fun isBubbleBarVisible(): Boolean
+
+    /**
+     * Updates the values of the internal animators after the new bubble animation was interrupted
+     *
+     * @param isStashed whether the current state should be stashed
+     * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
+     *   bubble bar is showing to ensure that the stash animator runs smoothly.
+     */
+    fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float)
+
+    /** Checks whether the motion event is over the stash handle or bubble bar. */
+    fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean
+
+    /** 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.
+     */
+    fun stashBubbleBar()
+
+    /** Shows the bubble bar, and expands bubbles depending on [expandBubbles]. */
+    fun showBubbleBar(expandBubbles: Boolean)
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /**
+     * The difference on the Y axis between the center of the handle and the center of the bubble
+     * bar.
+     */
+    fun getDiffBetweenHandleAndBarCenters(): Float
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** The distance the handle moves as part of the new bubble animation. */
+    fun getStashedHandleTranslationForNewBubbleAnimation(): Float
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** Returns the [PhysicsAnimator] for the stashed handle view. */
+    fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>?
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** Notifies taskbar that it should update its touchable region. */
+    fun updateTaskbarTouchRegion()
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** Set the translation Y for the stashed handle. */
+    fun setHandleTranslationY(translationY: Float)
+
+    /** Returns the translation of the handle. */
+    fun getHandleTranslationY(): Float?
+
+    /** Returns bounds of the handle */
+    fun getHandleBounds(bounds: Rect)
+
+    /**
+     * Returns bubble bar Y position according to [isBubblesShowingOnHome] and
+     * [isBubblesShowingOnOverview] values. Default implementation only analyse
+     * [isBubblesShowingOnHome] and return translationY to align with the hotseat vertical center.
+     * For Other cases align bubbles with the taskbar.
+     */
+    val bubbleBarTranslationY: Float
+        get() =
+            if (isBubblesShowingOnHome) {
+                bubbleBarTranslationYForHotseat
+            } else {
+                bubbleBarTranslationYForTaskbar
+            }
+
+    /** Translation Y to align the bubble bar with the taskbar. */
+    val bubbleBarTranslationYForTaskbar: Float
+
+    /** 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:")
+        pw.println("  isStashed: $isStashed")
+        pw.println("  isBubblesShowingOnOverview: $isBubblesShowingOnOverview")
+        pw.println("  isBubblesShowingOnHome: $isBubblesShowingOnHome")
+        pw.println("  isSysuiLocked: $isSysuiLocked")
+    }
+
+    companion object {
+        /** How long to stash/unstash. */
+        const val BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE.toLong()
+
+        /** How long to translate Y coordinate of the BubbleBar. */
+        const val BAR_TRANSLATION_DURATION = 300L
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
new file mode 100644
index 0000000..886b9f0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.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.launcher3.taskbar.bubbles.stashing
+
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
+
+/**
+ * Implementation of the [TaskbarHotseatDimensionsProvider] that take sizes from the
+ * [DeviceProfile].
+ */
+class DeviceProfileDimensionsProviderAdapter(
+    private val taskbarActivityContext: TaskbarActivityContext
+) : TaskbarHotseatDimensionsProvider {
+    override fun getTaskbarBottomSpace(): Int = taskbarDp().taskbarBottomMargin
+
+    override fun getTaskbarHeight(): Int = taskbarDp().taskbarHeight
+
+    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
new file mode 100644
index 0000000..45f5568
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -0,0 +1,269 @@
+/*
+ * 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 android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+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
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+
+class PersistentBubbleStashController(
+    private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider
+) : BubbleStashController {
+
+    private lateinit var taskbarInsetsController: TaskbarInsetsController
+    private lateinit var bubbleBarViewController: BubbleBarViewController
+    private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat
+    private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory<View>.MultiProperty
+    private lateinit var bubbleBarScaleAnimator: AnimatedFloat
+    private lateinit var controllersAfterInitAction: ControllersAfterInitAction
+    private var hotseatVerticalCenter: Int = 0
+
+    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 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()
+            }
+        }
+
+    override var isSysuiLocked: Boolean = false
+        set(isLocked) {
+            if (field == isLocked) return
+            field = isLocked
+            if (!isLocked && bubbleBarViewController.hasBubbles()) {
+                animateAfterUnlock()
+            }
+        }
+
+    override var isTransientTaskBar: Boolean = false
+
+    /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+    override val hasHandleView: Boolean = false
+
+    /** For persistent task bar we never stash the bubble bar */
+    override val isStashed: Boolean = false
+
+    override val bubbleBarTranslationYForTaskbar: Float
+        get() {
+            val taskbarBottomMargin = taskbarHotseatDimensionsProvider.getTaskbarBottomSpace()
+            val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
+            val taskbarHeight = taskbarHotseatDimensionsProvider.getTaskbarHeight()
+            return -taskbarBottomMargin - (taskbarHeight - bubbleBarHeight) / 2f
+        }
+
+    override val bubbleBarTranslationYForHotseat: Float
+        get() {
+            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,
+    ) {
+        this.taskbarInsetsController = taskbarInsetsController
+        this.bubbleBarViewController = bubbleBarViewController
+        this.controllersAfterInitAction = controllersAfterInitAction
+        bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
+        // bubble bar has only alpha property, getting it at index 0
+        bubbleBarAlphaAnimator = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0)
+        bubbleBarScaleAnimator = bubbleBarViewController.bubbleBarScaleY
+    }
+
+    private fun animateAfterUnlock() {
+        val animatorSet = AnimatorSet()
+        if (isBubblesShowingOnHome || isBubblesShowingOnOverview) {
+            animatorSet.playTogether(
+                bubbleBarScaleAnimator.animateToValue(1f),
+                bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
+                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) {
+        bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
+        bubbleBarAlphaAnimator.setValue(1f)
+        bubbleBarScaleAnimator.updateValue(1f)
+    }
+
+    override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {
+        // When the bubble bar is shown for the persistent task bar, there is no handle view, so no
+        // operation is performed.
+    }
+
+    override fun stashBubbleBar() {
+        updateExpandedState(expand = false)
+    }
+
+    override fun showBubbleBar(expandBubbles: Boolean) {
+        updateExpandedState(expandBubbles)
+    }
+
+    override fun stashBubbleBarImmediate() {
+        // When the bubble bar is shown for the persistent task bar, there is no handle view, so no
+        // operation is performed.
+    }
+
+    /** If bubble bar is visible return bubble bar height, 0 otherwise */
+    override fun getTouchableHeight() =
+        if (isBubbleBarVisible()) {
+            bubbleBarViewController.bubbleBarCollapsedHeight.toInt()
+        } else {
+            0
+        }
+
+    override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles()
+
+    override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) {
+        showBubbleBarImmediate(bubbleBarTranslationY)
+    }
+
+    override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean =
+        bubbleBarViewController.isEventOverAnyItem(ev)
+
+    override fun getDiffBetweenHandleAndBarCenters(): Float {
+        // distance from the bottom of the screen and the bubble bar center.
+        return -bubbleBarViewController.bubbleBarCollapsedHeight / 2f
+    }
+
+    /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+    override fun getStashedHandleTranslationForNewBubbleAnimation(): Float = 0f
+
+    /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+    override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? = null
+
+    override fun updateTaskbarTouchRegion() {
+        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    /**
+     * When the bubble bar is shown for the persistent task bar the bar does not stash, so no
+     * operation is performed
+     */
+    override fun setHandleTranslationY(translationY: Float) {
+        // no op since does not have a handle view
+    }
+
+    override fun getHandleTranslationY(): Float? = null
+
+    override fun getHandleBounds(bounds: Rect) {
+        // no op since does not have a handle view
+    }
+
+    private fun updateExpandedState(expand: Boolean) {
+        if (bubbleBarViewController.isHiddenForNoBubbles) {
+            // If there are no bubbles the bar is invisible, nothing to do here.
+            return
+        }
+        if (bubbleBarViewController.isExpanded != expand) {
+            bubbleBarViewController.isExpanded = expand
+        }
+    }
+
+    /** Animates bubble bar Y accordingly to the showing mode */
+    private fun animateBubbleBarY() {
+        val animator =
+            bubbleBarViewController.bubbleBarTranslationY.animateToValue(bubbleBarTranslationY)
+        updateTouchRegionOnAnimationEnd(animator)
+        animator.setDuration(BAR_TRANSLATION_DURATION)
+        animator.start()
+    }
+
+    private fun updateTouchRegionOnAnimationEnd(animator: Animator) {
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+
+                override fun onAnimationEnd(animation: Animator) {
+                    controllersAfterInitAction.runAfterInit {
+                        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+                    }
+                }
+            }
+        )
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
new file mode 100644
index 0000000..e62c0d4
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -0,0 +1,536 @@
+/*
+ * 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 android.animation.Animator
+import android.animation.AnimatorSet
+import android.content.Context
+import android.graphics.Rect
+import android.view.MotionEvent
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.app.animation.Interpolators.EMPHASIZED
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.R
+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_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
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import kotlin.math.max
+
+class TransientBubbleStashController(
+    private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
+    private val context: Context,
+) : BubbleStashController {
+
+    private lateinit var bubbleBarViewController: BubbleBarViewController
+    private lateinit var taskbarInsetsController: TaskbarInsetsController
+    private lateinit var controllersAfterInitAction: ControllersAfterInitAction
+
+    // stash view properties
+    private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
+    private var stashHandleViewAlpha: MultiPropertyFactory<View>.MultiProperty? = null
+    private var translationYDuringStash = AnimatedFloat { transY ->
+        bubbleStashedHandleViewController?.setTranslationYForStash(transY)
+        bubbleBarViewController.setTranslationYForStash(transY)
+    }
+    private val stashHandleStashVelocity =
+        context.resources.getDimension(R.dimen.bubblebar_stashed_handle_spring_velocity_dp_per_s)
+    private var stashedHeight: Int = 0
+
+    // bubble bar properties
+    private lateinit var bubbleBarAlpha: MultiPropertyFactory<View>.MultiProperty
+    private lateinit var bubbleBarBubbleAlpha: AnimatedFloat
+    private lateinit var bubbleBarBackgroundAlpha: AnimatedFloat
+    private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat
+    private lateinit var bubbleBarBubbleTranslationY: AnimatedFloat
+    private lateinit var bubbleBarBackgroundScaleX: AnimatedFloat
+    private lateinit var bubbleBarBackgroundScaleY: AnimatedFloat
+    private val handleCenterFromScreenBottom =
+        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 launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP
+        set(state) {
+            if (field == state) return
+            field = state
+            val hasBubbles = bubbleBarViewController.hasBubbles()
+            bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
+            if (!hasBubbles) {
+                // if there are no bubbles, there's nothing to show, so just return.
+                return
+            }
+            if (field == BubbleLauncherState.HOME) {
+                // When to home we need to animate the bubble bar
+                // here to align with hotseat center.
+                animateBubbleBarYToHotseat()
+            } else if (field == BubbleLauncherState.OVERVIEW) {
+                // When transitioning to overview we need to animate the bubble bar to align with
+                // the taskbar bottom.
+                animateBubbleBarYToTaskbar()
+            }
+            // 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
+        set(isLocked) {
+            if (field == isLocked) return
+            field = isLocked
+            if (!isLocked && bubbleBarViewController.hasBubbles()) {
+                animateAfterUnlock()
+            }
+        }
+
+    override val isTransientTaskBar: Boolean = true
+
+    override val bubbleBarTranslationYForHotseat: Float
+        get() {
+            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
+
+    override fun init(
+        taskbarInsetsController: TaskbarInsetsController,
+        bubbleBarViewController: BubbleBarViewController,
+        bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+        controllersAfterInitAction: ControllersAfterInitAction,
+    ) {
+        this.taskbarInsetsController = taskbarInsetsController
+        this.bubbleBarViewController = bubbleBarViewController
+        this.bubbleStashedHandleViewController = bubbleStashedHandleViewController
+        this.controllersAfterInitAction = controllersAfterInitAction
+        bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
+        bubbleBarBubbleTranslationY = bubbleBarViewController.bubbleOffsetY
+        // bubble bar has only alpha property, getting it at index 0
+        bubbleBarAlpha = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0)
+        bubbleBarBubbleAlpha = bubbleBarViewController.bubbleBarBubbleAlpha
+        bubbleBarBackgroundAlpha = bubbleBarViewController.bubbleBarBackgroundAlpha
+        bubbleBarBackgroundScaleX = bubbleBarViewController.bubbleBarBackgroundScaleX
+        bubbleBarBackgroundScaleY = bubbleBarViewController.bubbleBarBackgroundScaleY
+        stashedHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0
+        stashHandleViewAlpha = bubbleStashedHandleViewController?.stashedHandleAlpha?.get(0)
+    }
+
+    private fun animateAfterUnlock() {
+        val animatorSet = AnimatorSet()
+        if (isBubblesShowingOnHome || isBubblesShowingOnOverview) {
+            isStashed = false
+            animatorSet.playTogether(
+                bubbleBarBackgroundScaleX.animateToValue(1f),
+                bubbleBarBackgroundScaleY.animateToValue(1f),
+                bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
+                bubbleBarAlpha.animateToValue(1f),
+                bubbleBarBubbleAlpha.animateToValue(1f),
+                bubbleBarBackgroundAlpha.animateToValue(1f),
+            )
+        } else {
+            isStashed = true
+            stashHandleViewAlpha?.let { animatorSet.playTogether(it.animateToValue(1f)) }
+        }
+        animatorSet
+            .updateBarVisibility(isStashed)
+            .updateTouchRegionOnAnimationEnd()
+            .setDuration(BAR_STASH_DURATION)
+            .start()
+    }
+
+    override fun setHotseatVerticalCenter(hotseatVerticalCenter: Int) {
+        this.hotseatVerticalCenter = hotseatVerticalCenter
+    }
+
+    override fun showBubbleBarImmediate() {
+        showBubbleBarImmediate(bubbleBarTranslationY)
+    }
+
+    override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
+        bubbleStashedHandleViewController?.setTranslationYForSwipe(0f)
+        stashHandleViewAlpha?.value = 0f
+        this.bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
+        bubbleBarAlpha.setValue(1f)
+        bubbleBarBubbleAlpha.updateValue(1f)
+        bubbleBarBackgroundAlpha.updateValue(1f)
+        bubbleBarBackgroundScaleX.updateValue(1f)
+        bubbleBarBackgroundScaleY.updateValue(1f)
+        isStashed = false
+        bubbleBarViewController.setHiddenForStashed(false)
+        onIsStashedChanged()
+    }
+
+    override fun stashBubbleBarImmediate() {
+        bubbleStashedHandleViewController?.setTranslationYForSwipe(0f)
+        stashHandleViewAlpha?.value = 1f
+        this.bubbleBarTranslationYAnimator.updateValue(getStashTranslation())
+        bubbleBarAlpha.setValue(0f)
+        // Reset bubble and background alpha to 1 and only keep the bubble bar alpha at 0
+        bubbleBarBubbleAlpha.updateValue(1f)
+        bubbleBarBackgroundAlpha.updateValue(1f)
+        bubbleBarBackgroundScaleX.updateValue(getStashScaleX())
+        bubbleBarBackgroundScaleY.updateValue(getStashScaleY())
+        isStashed = true
+        bubbleBarViewController.setHiddenForStashed(true)
+        onIsStashedChanged()
+    }
+
+    override fun getTouchableHeight(): Int =
+        when {
+            isStashed -> stashedHeight
+            isBubbleBarVisible() -> bubbleBarViewController.bubbleBarCollapsedHeight.toInt()
+            else -> 0
+        }
+
+    override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles() && !isStashed
+
+    override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) =
+        if (isStashed) {
+            stashBubbleBarImmediate()
+        } else {
+            showBubbleBarImmediate(bubbleBarTranslationY)
+        }
+
+    /** Check if [ev] belongs to the stash handle or the bubble bar views. */
+    override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean {
+        val isOverHandle = bubbleStashedHandleViewController?.isEventOverHandle(ev) ?: false
+        return isOverHandle || bubbleBarViewController.isEventOverAnyItem(ev)
+    }
+
+    /** Set the bubble bar stash handle location . */
+    override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {
+        bubbleStashedHandleViewController?.setBubbleBarLocation(bubbleBarLocation)
+    }
+
+    override fun stashBubbleBar() {
+        updateStashedAndExpandedState(stash = true, expand = false)
+    }
+
+    override fun showBubbleBar(expandBubbles: Boolean) {
+        updateStashedAndExpandedState(stash = false, expandBubbles)
+    }
+
+    override fun getDiffBetweenHandleAndBarCenters(): Float {
+        // the difference between the centers of the handle and the bubble bar is the difference
+        // between their distance from the bottom of the screen.
+        val barCenter: Float = bubbleBarViewController.bubbleBarCollapsedHeight / 2f
+        return handleCenterFromScreenBottom - barCenter
+    }
+
+    override fun getStashedHandleTranslationForNewBubbleAnimation(): Float {
+        return -handleCenterFromScreenBottom
+    }
+
+    override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? {
+        return bubbleStashedHandleViewController?.physicsAnimator
+    }
+
+    override fun updateTaskbarTouchRegion() {
+        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    override fun setHandleTranslationY(translationY: Float) {
+        bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY)
+    }
+
+    override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY
+
+    override fun getHandleBounds(bounds: Rect) {
+        bubbleStashedHandleViewController?.getBounds(bounds)
+    }
+
+    private fun getStashTranslation(): Float {
+        return (bubbleBarTranslationY - stashedHeight) / 2f
+    }
+
+    @VisibleForTesting
+    fun getStashScaleX(): Float {
+        val handleWidth = bubbleStashedHandleViewController?.stashedWidth ?: 0
+        return handleWidth / bubbleBarViewController.bubbleBarCollapsedWidth
+    }
+
+    @VisibleForTesting
+    fun getStashScaleY(): Float {
+        val handleHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0
+        return handleHeight / bubbleBarViewController.bubbleBarCollapsedHeight
+    }
+
+    /**
+     * Create a stash animation.
+     *
+     * @param isStashed whether it's a stash animation or an unstash animation
+     * @param duration duration of the animation
+     * @return the animation
+     */
+    @Suppress("SameParameterValue")
+    private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet {
+        val animatorSet = AnimatorSet()
+
+        animatorSet.play(
+            createBackgroundAlphaAnimator(isStashed).apply {
+                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
+                this.interpolator = LINEAR
+            }
+        )
+
+        animatorSet.play(
+            bubbleBarBubbleAlpha
+                .animateToValue(getBarAlphaStart(isStashed), getBarAlphaEnd(isStashed))
+                .apply {
+                    this.duration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
+                    this.startDelay = TASKBAR_STASH_ALPHA_START_DELAY
+                    this.interpolator = LINEAR
+                }
+        )
+
+        animatorSet.play(
+            createSpringOnStashAnimator(isStashed).apply {
+                this.duration = duration
+                this.interpolator = LINEAR
+            }
+        )
+
+        animatorSet.play(
+            bubbleBarViewController.createRevealAnimatorForStashChange(isStashed).apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+            }
+        )
+
+        // Animate bubble translation to keep reveal animation in the bounds of the bar
+        val bubbleTyStart = if (isStashed) 0f else -bubbleBarTranslationY
+        val bubbleTyEnd = if (isStashed) -bubbleBarTranslationY else 0f
+        animatorSet.play(
+            bubbleBarBubbleTranslationY.animateToValue(bubbleTyStart, bubbleTyEnd).apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+            }
+        )
+
+        animatorSet.play(
+            bubbleStashedHandleViewController?.createRevealAnimToIsStashed(isStashed)?.apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+            }
+        )
+
+        val pivotX = if (bubbleBarViewController.isBubbleBarOnLeft) 0f else 1f
+        animatorSet.play(
+            createScaleAnimator(isStashed).apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+                this.setBubbleBarPivotDuringAnim(pivotX, 1f)
+            }
+        )
+
+        val translationYTarget = if (isStashed) getStashTranslation() else bubbleBarTranslationY
+        animatorSet.play(
+            bubbleBarTranslationYAnimator.animateToValue(translationYTarget).apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+            }
+        )
+
+        animatorSet.doOnStart {
+            // Update the start value for bubble view and background alpha when the entire animation
+            // begins.
+            // Alpha animation has a delay, and if we set the initial values at the start of the
+            // alpha animation, it will cause flickers.
+            bubbleBarBubbleAlpha.updateValue(getBarAlphaStart(isStashed))
+            bubbleBarBackgroundAlpha.updateValue(getBarAlphaStart(isStashed))
+            // We animate alpha for background and bubble views separately. Make sure the container
+            // is always visible.
+            bubbleBarAlpha.value = 1f
+        }
+        animatorSet.doOnEnd {
+            animator = null
+            controllersAfterInitAction.runAfterInit {
+                if (isStashed) {
+                    bubbleBarAlpha.value = 0f
+                    // reset bubble view alpha
+                    bubbleBarBubbleAlpha.updateValue(1f)
+                    bubbleBarBackgroundAlpha.updateValue(1f)
+                    // reset stash translation
+                    translationYDuringStash.updateValue(0f)
+                    bubbleBarBubbleTranslationY.updateValue(0f)
+                    bubbleBarViewController.isExpanded = false
+                }
+                taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+            }
+        }
+        return animatorSet
+    }
+
+    private fun createBackgroundAlphaAnimator(isStashed: Boolean): AnimatorSet {
+        return AnimatorSet().apply {
+            play(
+                bubbleBarBackgroundAlpha.animateToValue(
+                    getBarAlphaStart(isStashed),
+                    getBarAlphaEnd(isStashed),
+                )
+            )
+            play(stashHandleViewAlpha?.animateToValue(getHandleAlphaEnd(isStashed)))
+        }
+    }
+
+    private fun getBarAlphaStart(isStashed: Boolean): Float {
+        return if (isStashed) 1f else 0f
+    }
+
+    private fun getBarAlphaEnd(isStashed: Boolean): Float {
+        return if (isStashed) 0f else 1f
+    }
+
+    private fun getHandleAlphaEnd(isStashed: Boolean): Float {
+        return if (isStashed) 1f else 0f
+    }
+
+    private fun createSpringOnStashAnimator(isStashed: Boolean): Animator {
+        if (!isStashed) {
+            // Animate the stash translation back to 0
+            return translationYDuringStash.animateToValue(0f)
+        }
+        // Apply a spring to the handle
+        return SpringAnimationBuilder(context)
+            .setStartValue(translationYDuringStash.value)
+            .setEndValue(0f)
+            .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+            .setStiffness(SpringForce.STIFFNESS_LOW)
+            .setStartVelocity(stashHandleStashVelocity)
+            .build(translationYDuringStash, AnimatedFloat.VALUE)
+    }
+
+    private fun createScaleAnimator(isStashed: Boolean): AnimatorSet {
+        val scaleXTarget = if (isStashed) getStashScaleX() else 1f
+        val scaleYTarget = if (isStashed) getStashScaleY() else 1f
+        return AnimatorSet().apply {
+            play(bubbleBarBackgroundScaleX.animateToValue(scaleXTarget))
+            play(bubbleBarBackgroundScaleY.animateToValue(scaleYTarget))
+        }
+    }
+
+    private fun onIsStashedChanged() {
+        controllersAfterInitAction.runAfterInit {
+            taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+            bubbleStashedHandleViewController?.onIsStashedChanged()
+        }
+    }
+
+    private fun animateBubbleBarYToHotseat() {
+        translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForHotseat)
+    }
+
+    private fun animateBubbleBarYToTaskbar() {
+        translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForTaskbar)
+    }
+
+    private fun translateBubbleBarYUpdateTouchRegionOnCompletion(toY: Float) {
+        bubbleBarViewController.bubbleBarTranslationY
+            .animateToValue(toY)
+            .updateTouchRegionOnAnimationEnd()
+            .setDuration(BAR_TRANSLATION_DURATION)
+            .start()
+    }
+
+    @VisibleForTesting
+    fun updateStashedAndExpandedState(stash: Boolean, expand: Boolean) {
+        if (bubbleBarViewController.isHiddenForNoBubbles) {
+            // If there are no bubbles the bar and handle are invisible, nothing to do here.
+            return
+        }
+        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()
+                }
+        }
+        if (bubbleBarViewController.isExpanded != expand) {
+            bubbleBarViewController.isExpanded = expand
+        }
+    }
+
+    private fun Animator.updateTouchRegionOnAnimationEnd(): Animator {
+        doOnEnd { onIsStashedChanged() }
+        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
+        doOnStart {
+            initialPivotX = bubbleBarViewController.bubbleBarRelativePivotX
+            initialPivotY = bubbleBarViewController.bubbleBarRelativePivotY
+            bubbleBarViewController.setBubbleBarRelativePivot(pivotX, pivotY)
+        }
+        doOnEnd {
+            if (!initialPivotX.isNaN() && !initialPivotY.isNaN()) {
+                bubbleBarViewController.setBubbleBarRelativePivot(initialPivotX, initialPivotY)
+            }
+        }
+        return this
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt b/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt
new file mode 100644
index 0000000..e384586
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.customization
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.launcher3.Insettable
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.views.ActivityContext
+
+/** TaskbarView that is customizeable via Taskbar containers. */
+class CustomizableTaskbarView(context: Context, attrs: AttributeSet? = null) :
+    ConstraintLayout(context, attrs), Insettable {
+    private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
+
+    init {
+        inflate(context, R.layout.customizable_taskbar_view, this)
+    }
+
+    override fun setInsets(insets: Rect?) {
+        // Ignore, we just implement Insettable to draw behind system insets.
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
new file mode 100644
index 0000000..c5f8aa0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.customization
+
+import android.annotation.SuppressLint
+import android.content.Context
+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
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarViewCallbacks
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.views.ActivityContext
+import com.android.launcher3.views.IconButtonView
+import com.android.quickstep.DeviceConfigWrapper
+import com.android.quickstep.util.ContextualSearchStateManager
+
+/** Taskbar all apps button container for customizable taskbar. */
+class TaskbarAllAppsButtonContainer
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    IconButtonView(context, attrs), TaskbarContainer {
+
+    private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
+    private var allAppsTouchTriggered = false
+    private var allAppsTouchRunnable: Runnable? = null
+    private var allAppsButtonTouchDelayMs: Long = ViewConfiguration.getLongPressTimeout().toLong()
+    private lateinit var taskbarViewCallbacks: TaskbarViewCallbacks
+
+    override val spaceNeeded: Int
+        get() {
+            return dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconSize.size.toFloat())
+        }
+
+    init {
+        LayoutInflater.from(context).inflate(R.layout.taskbar_all_apps_button, null, false)
+        setUpIcon()
+    }
+
+    @SuppressLint("UseCompatLoadingForDrawables", "ResourceAsColor")
+    private fun setUpIcon() {
+        val drawable =
+            resources.getDrawable(
+                getAllAppsButton(activityContext.taskbarFeatureEvaluator.isTransient)
+            )
+        backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
+        setIconDrawable(drawable)
+        setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
+        setForegroundTint(activityContext.getColor(R.color.all_apps_button_color))
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    fun setUpCallbacks(callbacks: TaskbarViewCallbacks) {
+        taskbarViewCallbacks = callbacks
+        setOnClickListener(this::onAllAppsButtonClick)
+        setOnLongClickListener(this::onAllAppsButtonLongClick)
+        setOnTouchListener(this::onAllAppsButtonTouch)
+        isHapticFeedbackEnabled =
+            taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled(mContext)
+        allAppsTouchRunnable = Runnable {
+            taskbarViewCallbacks.triggerAllAppsButtonLongClick()
+            allAppsTouchTriggered = true
+        }
+        val contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[mContext]
+        if (
+            DeviceConfigWrapper.get().customLpaaThresholds &&
+                contextualSearchStateManager.lpnhDurationMillis.isPresent
+        ) {
+            allAppsButtonTouchDelayMs = contextualSearchStateManager.lpnhDurationMillis.get()
+        }
+    }
+
+    @DrawableRes
+    private fun getAllAppsButton(isTransientTaskbar: Boolean): Int {
+        val shouldSelectTransientIcon =
+            isTransientTaskbar || (enableTaskbarPinning() && !activityContext.isThreeButtonNav)
+        return if (shouldSelectTransientIcon) R.drawable.ic_transient_taskbar_all_apps_search_button
+        else R.drawable.ic_taskbar_all_apps_search_button
+    }
+
+    @DimenRes
+    fun getAllAppsButtonTranslationXOffset(isTransientTaskbar: Boolean): Int {
+        return if (isTransientTaskbar) {
+            R.dimen.transient_taskbar_all_apps_button_translation_x_offset
+        } else {
+            R.dimen.taskbar_all_apps_search_button_translation_x_offset
+        }
+    }
+
+    private fun onAllAppsButtonTouch(view: View, ev: MotionEvent): Boolean {
+        when (ev.action) {
+            MotionEvent.ACTION_DOWN -> {
+                allAppsTouchTriggered = false
+                MAIN_EXECUTOR.handler.postDelayed(allAppsTouchRunnable!!, allAppsButtonTouchDelayMs)
+            }
+            MotionEvent.ACTION_UP,
+            MotionEvent.ACTION_CANCEL -> cancelAllAppsButtonTouch()
+        }
+        return false
+    }
+
+    private fun cancelAllAppsButtonTouch() {
+        MAIN_EXECUTOR.handler.removeCallbacks(allAppsTouchRunnable!!)
+        // ACTION_UP is first triggered, then click listener / long-click listener is triggered on
+        // the next frame, so we need to post twice and delay the reset.
+        this.post { this.post { allAppsTouchTriggered = false } }
+    }
+
+    private fun onAllAppsButtonClick(view: View) {
+        if (!allAppsTouchTriggered) {
+            taskbarViewCallbacks.triggerAllAppsButtonClick(view)
+        }
+    }
+
+    // Handle long click from Switch Access and Voice Access
+    private fun onAllAppsButtonLongClick(view: View): Boolean {
+        if (!MAIN_EXECUTOR.handler.hasCallbacks(allAppsTouchRunnable!!) && !allAppsTouchTriggered) {
+            taskbarViewCallbacks.triggerAllAppsButtonLongClick()
+        }
+        return true
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt
index 3c4b63a..35ae43c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt
@@ -13,15 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.launcher3.taskbar.customization
 
-/** Enums for all feature container that taskbar supports. */
-enum class TaskbarContainer {
-    ALL_APPS,
-    DIVIDER,
-    APP_ICONS,
-    RECENTS,
-    NAV_BUTTONS,
-    BUBBLES,
+import androidx.annotation.Dimension
+
+/**
+ * Interface to be implemented by all taskbar container to expose [spaceNeeded] for each container.
+ */
+interface TaskbarContainer {
+    @get:Dimension(unit = Dimension.DP) val spaceNeeded: Int
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt
new file mode 100644
index 0000000..d4548f5
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.customization
+
+/** Enums for all feature container that taskbar supports. */
+enum class TaskbarContainers {
+    ALL_APPS,
+    DIVIDER,
+    APP_ICONS,
+    RECENTS,
+    NAV_BUTTONS,
+    BUBBLES,
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
new file mode 100644
index 0000000..1fb835a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.customization
+
+import android.annotation.SuppressLint
+import android.content.Context
+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
+import com.android.launcher3.taskbar.TaskbarViewCallbacks
+import com.android.launcher3.views.ActivityContext
+import com.android.launcher3.views.IconButtonView
+
+/** Taskbar divider view container for customizable taskbar. */
+class TaskbarDividerContainer
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+) : IconButtonView(context, attrs), TaskbarContainer {
+    private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
+
+    override val spaceNeeded: Int
+        get() {
+            return dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconSize.size.toFloat())
+        }
+
+    init {
+        LayoutInflater.from(context).inflate(R.layout.taskbar_divider, null, false)
+        setUpIcon()
+    }
+
+    @SuppressLint("UseCompatLoadingForDrawables")
+    fun setUpIcon() {
+        backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
+        val drawable = resources.getDrawable(R.drawable.taskbar_divider_button)
+        setIconDrawable(drawable)
+        setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    fun setUpCallbacks(callbacks: TaskbarViewCallbacks) {
+        setOnLongClickListener(callbacks.taskbarDividerLongClickListener)
+        setOnTouchListener(callbacks.taskbarDividerRightClickListener)
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
index ac7dd06..f130d29 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
@@ -16,31 +16,49 @@
 
 package com.android.launcher3.taskbar.customization
 
+import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.launcher3.taskbar.TaskbarControllers
 import com.android.launcher3.util.DisplayController
 
 /** Evaluates all the features taskbar can have. */
-class TaskbarFeatureEvaluator(
-    private val taskbarActivityContext: TaskbarActivityContext,
-    private val taskbarControllers: TaskbarControllers,
-) {
-
+class TaskbarFeatureEvaluator
+private constructor(private val taskbarActivityContext: TaskbarActivityContext) {
     val hasAllApps = true
     val hasAppIcons = true
     val hasBubbles = false
     val hasNavButtons = taskbarActivityContext.isThreeButtonNav
 
-    val hasRecents: Boolean
-        get() = taskbarControllers.taskbarRecentAppsController.shownTasks.isNotEmpty()
+    val isRecentsEnabled: Boolean
+        get() = enableRecentsInTaskbar()
 
     val hasDivider: Boolean
-        get() = enableTaskbarPinning() || hasRecents
+        get() = enableTaskbarPinning() || isRecentsEnabled
 
     val isTransient: Boolean
         get() = DisplayController.isTransientTaskbar(taskbarActivityContext)
 
     val isLandscape: Boolean
         get() = taskbarActivityContext.deviceProfile.isLandscape
+
+    val supportsPinningPopup: Boolean
+        get() = !hasNavButtons
+
+    fun onDestroy() {
+        taskbarFeatureEvaluator = null
+    }
+
+    companion object {
+        @Volatile private var taskbarFeatureEvaluator: TaskbarFeatureEvaluator? = null
+
+        @JvmStatic
+        fun getInstance(taskbarActivityContext: TaskbarActivityContext): TaskbarFeatureEvaluator {
+            synchronized(this) {
+                if (taskbarFeatureEvaluator == null) {
+                    taskbarFeatureEvaluator = TaskbarFeatureEvaluator(taskbarActivityContext)
+                }
+                return taskbarFeatureEvaluator!!
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt
index 67bbcce..e55cb1f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt
@@ -19,16 +19,26 @@
 /** Taskbar Icon Specs */
 object TaskbarIconSpecs {
 
-    val iconSize40dp = TaskbarIconSize(40)
-    val iconSize44dp = TaskbarIconSize(44)
-    val iconSize48dp = TaskbarIconSize(48)
-    val iconSize52dp = TaskbarIconSize(52)
+    // Mapping of visual icon size to icon specs value http://b/235886078
+    val iconSize40dp = TaskbarIconSize(44)
+    val iconSize44dp = TaskbarIconSize(48)
+    val iconSize48dp = TaskbarIconSize(52)
+    val iconSize52dp = TaskbarIconSize(57)
 
     val transientTaskbarIconSizes = arrayOf(iconSize44dp, iconSize48dp, iconSize52dp)
 
     val defaultPersistentIconSize = iconSize40dp
     val defaultTransientIconSize = iconSize44dp
 
+    val minimumIconSize = iconSize40dp
+
+    val defaultPersistentIconMargin = TaskbarIconMarginSize(6)
+    val defaultTransientIconMargin = TaskbarIconMarginSize(12)
+
+    val minimumTaskbarIconTouchSize = TaskbarIconSize(48)
+
+    val transientOrPinnedTaskbarIconPaddingSize = iconSize52dp
+
     val transientTaskbarIconSizeByGridSize =
         mapOf(
             TransientTaskbarIconSizeKey(6, 5, false) to iconSize52dp,
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
index 0b7be40..822ca64 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
@@ -16,13 +16,41 @@
 
 package com.android.launcher3.taskbar.customization
 
-/** Evaluates the taskbar specs based on the taskbar grid size and the taskbar icon size. */
-class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator) {
+import com.android.launcher3.taskbar.TaskbarActivityContext
 
-    fun getIconSizeByGrid(row: Int, column: Int): TaskbarIconSize {
+/** Evaluates the taskbar specs based on the taskbar grid size and the taskbar icon size. */
+class TaskbarSpecsEvaluator(
+    private val taskbarActivityContext: TaskbarActivityContext,
+    private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator,
+    numRows: Int = taskbarActivityContext.deviceProfile.inv.numRows,
+    numColumns: Int = taskbarActivityContext.deviceProfile.inv.numColumns,
+) {
+    var taskbarIconSize: TaskbarIconSize = getIconSizeByGrid(numColumns, numRows)
+
+    // TODO(b/341146605) : initialize it to taskbar container in later cl.
+    private var taskbarContainer: List<TaskbarContainer> = emptyList()
+
+    val taskbarIconPadding: Int =
+        if (
+            TaskbarIconSpecs.transientOrPinnedTaskbarIconPaddingSize.size > taskbarIconSize.size &&
+                !taskbarFeatureEvaluator.hasNavButtons
+        ) {
+            (TaskbarIconSpecs.iconSize52dp.size - taskbarIconSize.size) / 2
+        } else {
+            0
+        }
+
+    val taskbarIconMargin: TaskbarIconMarginSize =
+        if (taskbarFeatureEvaluator.isTransient) {
+            TaskbarIconSpecs.defaultTransientIconMargin
+        } else {
+            TaskbarIconSpecs.defaultPersistentIconMargin
+        }
+
+    fun getIconSizeByGrid(columns: Int, rows: Int): TaskbarIconSize {
         return if (taskbarFeatureEvaluator.isTransient) {
             TaskbarIconSpecs.transientTaskbarIconSizeByGridSize.getOrDefault(
-                TransientTaskbarIconSizeKey(row, column, taskbarFeatureEvaluator.isLandscape),
+                TransientTaskbarIconSizeKey(columns, rows, taskbarFeatureEvaluator.isLandscape),
                 TaskbarIconSpecs.defaultTransientIconSize,
             )
         } else {
@@ -36,8 +64,11 @@
         val currentIconSizeIndex = TaskbarIconSpecs.transientTaskbarIconSizes.indexOf(iconSize)
         // return the current icon size if supplied icon size is unknown or we have reached the
         // min icon size.
-        return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) iconSize
-        else TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1]
+        return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) {
+            iconSize
+        } else {
+            TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1]
+        }
     }
 
     fun getIconSizeStepUp(iconSize: TaskbarIconSize): TaskbarIconSize {
@@ -52,11 +83,28 @@
         ) {
             iconSize
         } else {
-            TaskbarIconSpecs.transientTaskbarIconSizes.get(currentIconSizeIndex + 1)
+            TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex + 1]
         }
     }
+
+    // TODO(jagrutdesai) : Call this in init once the containers are ready.
+    private fun calculateTaskbarIconSize() {
+        while (
+            taskbarIconSize != TaskbarIconSpecs.minimumIconSize &&
+                taskbarActivityContext.transientTaskbarBounds.width() <
+                    calculateSpaceNeeded(taskbarContainer)
+        ) {
+            taskbarIconSize = getIconSizeStepDown(taskbarIconSize)
+        }
+    }
+
+    private fun calculateSpaceNeeded(containers: List<TaskbarContainer>): Int {
+        return containers.sumOf { it.spaceNeeded }
+    }
 }
 
 data class TaskbarIconSize(val size: Int)
 
-data class TransientTaskbarIconSizeKey(val row: Int, val column: Int, val isLandscape: Boolean)
+data class TransientTaskbarIconSizeKey(val columns: Int, val rows: Int, val isLandscape: Boolean)
+
+data class TaskbarIconMarginSize(val size: Int)
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 110ca16..4590efe 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ArgbEvaluator;
 import android.animation.Keyframe;
@@ -39,6 +40,7 @@
 import android.os.Process;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Property;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
@@ -71,12 +73,27 @@
  */
 public class PredictedAppIcon extends DoubleShadowBubbleTextView {
 
+    private static final float RING_SCALE_START_VALUE = 0.75f;
     private static final int RING_SHADOW_COLOR = 0x99000000;
     private static final float RING_EFFECT_RATIO = 0.095f;
 
     private static final long ICON_CHANGE_ANIM_DURATION = 360;
     private static final long ICON_CHANGE_ANIM_STAGGER = 50;
 
+    private static final Property<PredictedAppIcon, Float> RING_SCALE_PROPERTY =
+            new Property<>(Float.TYPE, "ringScale") {
+                @Override
+                public Float get(PredictedAppIcon icon) {
+                    return icon.mRingScale;
+                }
+
+                @Override
+                public void set(PredictedAppIcon icon, Float value) {
+                    icon.mRingScale = value;
+                    icon.invalidate();
+                }
+            };
+
     boolean mIsDrawingDot = false;
     private final DeviceProfile mDeviceProfile;
     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -96,6 +113,11 @@
     private Animator mSlotMachineAnim;
     private float mSlotMachineIconTranslationY;
 
+    // Used to animate the "ring" around predicted icons
+    private float mRingScale = 1f;
+    private boolean mForceHideRing = false;
+    private Animator mRingScaleAnim;
+
     private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y =
             new FloatProperty<PredictedAppIcon>("slotMachineTranslationY") {
         @Override
@@ -356,17 +378,52 @@
         }
     }
 
+    @Override
+    public void setForceHideRing(boolean forceHideRing) {
+        if (mForceHideRing == forceHideRing) {
+            return;
+        }
+        mForceHideRing = forceHideRing;
+
+        if (forceHideRing) {
+            invalidate();
+        } else {
+            animateRingScale(RING_SCALE_START_VALUE, 1);
+        }
+    }
+
+    private void cancelRingScaleAnim() {
+        if (mRingScaleAnim != null) {
+            mRingScaleAnim.cancel();
+        }
+    }
+
+    private void animateRingScale(float... ringScale) {
+        cancelRingScaleAnim();
+        mRingScaleAnim = ObjectAnimator.ofFloat(this, RING_SCALE_PROPERTY, ringScale);
+        mRingScaleAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mRingScaleAnim = null;
+            }
+        });
+        mRingScaleAnim.start();
+    }
+
     private void drawEffect(Canvas canvas) {
-        // Don't draw ring effect if item is about to be dragged.
-        if (mDrawForDrag) {
+        // Don't draw ring effect if item is about to be dragged or if the icon is not visible.
+        if (mDrawForDrag || !mIsIconVisible || mForceHideRing) {
             return;
         }
         mIconRingPaint.setColor(RING_SHADOW_COLOR);
         mIconRingPaint.setMaskFilter(mShadowFilter);
+        int count = canvas.save();
+        canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
         canvas.drawPath(mRingPath, mIconRingPaint);
         mIconRingPaint.setColor(mPlateColor);
         mIconRingPaint.setMaskFilter(null);
         canvas.drawPath(mRingPath, mIconRingPaint);
+        canvas.restoreToCount(count);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
index 039c0a0..26a1322 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
@@ -22,7 +22,6 @@
 import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
 import android.content.Intent;
-import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.Pair;
@@ -30,7 +29,6 @@
 import android.widget.RemoteViews;
 import android.window.SplashScreen;
 
-import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.ActivityOptionsWrapper;
@@ -66,14 +64,8 @@
         }
         Pair<Intent, ActivityOptions> options = remoteResponse.getLaunchOptions(view);
         ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager()
-                .getActivityLaunchOptions(hostView);
-        Object itemInfo = hostView.getTag();
-        IBinder launchCookie = null;
-        if (itemInfo instanceof ItemInfo) {
-            launchCookie = mLauncher.getLaunchCookie((ItemInfo) itemInfo);
-            activityOptions.options.setLaunchCookie(launchCookie);
-        }
-        if (Utilities.ATLEAST_S && !pendingIntent.isActivity()) {
+                .getActivityLaunchOptions(hostView, (ItemInfo) hostView.getTag());
+        if (!pendingIntent.isActivity()) {
             // In the event this pending intent eventually launches an activity, i.e. a trampoline,
             // use the Quickstep transition animation.
             try {
@@ -81,7 +73,7 @@
                         .registerRemoteAnimationForNextActivityStart(
                                 pendingIntent.getCreatorPackage(),
                                 activityOptions.options.getRemoteAnimationAdapter(),
-                                launchCookie);
+                                activityOptions.options.getLaunchCookie());
             } catch (RemoteException e) {
                 // Do nothing.
             }
@@ -92,7 +84,7 @@
                 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
         options = Pair.create(options.first, activityOptions.options);
         if (pendingIntent.isActivity()) {
-            logAppLaunch(itemInfo);
+            logAppLaunch(hostView.getTag());
         }
         return RemoteViews.startPendingIntent(hostView, pendingIntent, options);
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index be6f690..34d9a68 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -19,10 +19,10 @@
 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.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;
-import static com.android.launcher3.Flags.enablePredictiveBackGesture;
 import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE;
@@ -37,14 +37,15 @@
 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;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED;
-import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
+import static com.android.launcher3.popup.SystemShortcut.BUBBLE_SHORTCUT;
 import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP;
 import static com.android.launcher3.popup.SystemShortcut.INSTALL;
 import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL;
@@ -60,14 +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.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
-import static com.android.wm.shell.common.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;
@@ -76,6 +75,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.ShortcutInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -83,7 +83,6 @@
 import android.media.permission.SafeCloseable;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.os.IRemoteCallback;
 import android.os.SystemProperties;
 import android.os.Trace;
@@ -140,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;
@@ -174,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;
@@ -190,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;
@@ -200,6 +201,10 @@
 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;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -213,9 +218,8 @@
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
-import kotlin.Unit;
-
-public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
+public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
+        SystemShortcut.BubbleActivityStarter {
     private static final boolean TRACE_LAYOUTS =
             SystemProperties.getBoolean("persist.debug.trace_layouts", false);
     private static final String TRACE_RELAYOUT_CLASS =
@@ -227,7 +231,6 @@
     private FixedContainerItems mAllAppsPredictions;
     private HotseatPredictionController mHotseatPredictionController;
     private DepthController mDepthController;
-    private @Nullable DesktopVisibilityController mDesktopVisibilityController;
     private QuickstepTransitionManager mAppTransitionManager;
 
     private OverviewActionsView<?> mActionsView;
@@ -240,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
@@ -256,6 +260,10 @@
 
     private boolean mIsPredictiveBackToHomeInProgress;
 
+    private boolean mCanShowAllAppsEducationView;
+
+    private boolean mIsOverlayVisible;
+
     public static QuickstepLauncher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -268,7 +276,7 @@
         RecentsView<?,?> overviewPanel = getOverviewPanel();
         SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
         mSplitSelectStateController =
-                new SplitSelectStateController(this, mHandler, getStateManager(),
+                new SplitSelectStateController(this, getStateManager(),
                         getDepthController(), getStatsLogManager(),
                         systemUiProxy, RecentsModel.INSTANCE.get(this),
                         () -> onStateBack());
@@ -276,7 +284,7 @@
         // TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
         OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
                 asContext(), deviceState);
-        if (enableDesktopWindowingMode()) {
+        if (DesktopModeStatus.canEnterDesktopMode(this)) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
                     getDepthController());
@@ -296,9 +304,7 @@
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
         mDepthController = new DepthController(this);
-        if (enableDesktopWindowingMode()) {
-            mDesktopVisibilityController = new DesktopVisibilityController(this);
-            mDesktopVisibilityController.registerSystemUiListener();
+        if (DesktopModeStatus.canEnterDesktopMode(this)) {
             mSplitSelectStateController.initSplitFromDesktopController(this,
                     overviewComponentObserver);
         }
@@ -462,6 +468,9 @@
         if (Flags.enablePrivateSpace()) {
             shortcuts.add(UNINSTALL_APP);
         }
+        if (enableBubbleAnything()) {
+            shortcuts.add(BUBBLE_SHORTCUT);
+        }
         return shortcuts.stream();
     }
 
@@ -489,11 +498,10 @@
         boolean started = ((getActivityFlags() & ACTIVITY_STATE_STARTED)) != 0;
         if (started) {
             DeviceProfile profile = getDeviceProfile();
-            boolean willUserBeActive =
-                    (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
             boolean visible = (state == NORMAL || state == OVERVIEW)
-                    && (willUserBeActive || isUserActive())
-                    && !profile.isVerticalBarLayout();
+                    && isUserActive()
+                    && !profile.isVerticalBarLayout()
+                    && !mIsOverlayVisible;
             SystemUiProxy.INSTANCE.get(this)
                     .setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx);
         }
@@ -503,6 +511,12 @@
     }
 
     @Override
+    public void onOverlayVisibilityChanged(boolean visible) {
+        super.onOverlayVisibilityChanged(visible);
+        mIsOverlayVisible = visible;
+    }
+
+    @Override
     public void bindExtraContainerItems(FixedContainerItems item) {
         if (item.containerId == Favorites.CONTAINER_PREDICTION) {
             mAllAppsPredictions = item;
@@ -513,7 +527,7 @@
         } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             mHotseatPredictionController.setPredictedItems(item);
         } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
-            getPopupDataProvider().setRecommendedWidgets(item.items);
+            getWidgetPickerDataProvider().setWidgetRecommendations(item.items);
         }
     }
 
@@ -542,10 +556,6 @@
             mLauncherUnfoldAnimationController.onDestroy();
         }
 
-        if (mDesktopVisibilityController != null) {
-            mDesktopVisibilityController.unregisterSystemUiListener();
-        }
-
         if (mSplitSelectStateController != null) {
             mSplitSelectStateController.onDestroy();
         }
@@ -590,13 +600,10 @@
                     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.launchTask(success -> {
+                    taskToLaunch.launchWithoutAnimation(success -> {
                         if (!success) {
                             getStateManager().goToState(OVERVIEW);
                         } else {
@@ -605,11 +612,7 @@
                         return Unit.INSTANCE;
                     });
                 } else {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "Quick switch from home failed: TaskViews at indices ")
-                                    .append(rv.getCurrentPage())
-                                    .append(" and 0 are missing."),
-                            QUICK_SWITCH_FROM_HOME_FAILED);
+                    ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFailed(rv.getCurrentPage());
                     getStateManager().goToState(NORMAL);
                 }
                 break;
@@ -676,12 +679,6 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        // Back dispatcher is registered in {@link BaseActivity#onCreate}. For predictive back to
-        // work, we must opt-in BEFORE registering back dispatcher. So we need to call
-        // setEnableOnBackInvokedCallback() before super.onCreate()
-        if (Utilities.ATLEAST_U && enablePredictiveBackGesture()) {
-            getApplicationInfo().setEnableOnBackInvokedCallback(true);
-        }
         super.onCreate(savedInstanceState);
         if (savedInstanceState != null) {
             mPendingSplitSelectInfo = ObjectWrapper.unwrap(
@@ -689,9 +686,7 @@
         }
         addMultiWindowModeChangedListener(mDepthController);
         initUnfoldTransitionProgressProvider();
-        if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
-            mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow());
-        }
+        mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow());
         getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE);
         QuickstepOnboardingPrefs.setup(this);
         View.setTraceLayoutSteps(TRACE_LAYOUTS);
@@ -712,7 +707,7 @@
         // Check if there is already an instance of this app running, if so, initiate the split
         // using that.
         mSplitSelectStateController.findLastActiveTasksAndRunCallback(
-                Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+                Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()),
                 false /* findExactPairMatch */,
                 foundTasks -> {
                     @Nullable Task foundTask = foundTasks[0];
@@ -739,7 +734,7 @@
         Rect tempRect = new Rect();
 
         mSplitSelectStateController.setInitialTaskSelect(source.intent,
-                source.position.stagePosition, source.itemInfo, source.splitEvent,
+                source.position.stagePosition, source.getItemInfo(), source.splitEvent,
                 source.alreadyRunningTaskId);
 
         RecentsView recentsView = getOverviewPanel();
@@ -757,6 +752,8 @@
         floatingTaskView.setOnClickListener(view ->
                 mSplitSelectStateController.getSplitAnimationController().
                         playAnimPlaceholderToFullscreen(this, view, Optional.empty()));
+        floatingTaskView.setContentDescription(source.getItemInfo().contentDescription);
+
         mSplitSelectStateController.setFirstFloatingTaskView(floatingTaskView);
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
@@ -891,8 +888,7 @@
         // event won't go through ViewRootImpl#InputStage#onProcess.
         // So when receive back key, try to do the same check thing in
         // ViewRootImpl#NativePreImeInputStage#onProcess
-        if (!Utilities.ATLEAST_U || !enablePredictiveBackGesture()
-                || event.getKeyCode() != KeyEvent.KEYCODE_BACK
+        if (event.getKeyCode() != KeyEvent.KEYCODE_BACK
                 || event.getAction() != KeyEvent.ACTION_UP || event.isCanceled()) {
             return false;
         }
@@ -903,18 +899,14 @@
 
     @Override
     protected void registerBackDispatcher() {
-        if (!enablePredictiveBackGesture()) {
-            super.registerBackDispatcher();
-            return;
-        }
         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();
                         }
@@ -924,7 +916,7 @@
 
                     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
                     @Override
-                    public void onBackInvoked() {
+                    public void onBackInvokedCompat() {
                         // Recreate mActiveOnBackAnimationCallback if necessary to avoid NPE
                         // because:
                         // 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not
@@ -940,7 +932,7 @@
                     }
 
                     @Override
-                    public void onBackProgressed(@NonNull BackEvent backEvent) {
+                    public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
@@ -949,7 +941,7 @@
                     }
 
                     @Override
-                    public void onBackCancelled() {
+                    public void onBackCancelledCompat() {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
@@ -1004,10 +996,11 @@
 
     @Override
     public void setResumed() {
-        if (!enableDesktopWindowingWallpaperActivity()
-                && mDesktopVisibilityController != null
-                && mDesktopVisibilityController.areDesktopTasksVisible()
-                && !mDesktopVisibilityController.isRecentsGestureInProgress()) {
+        DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
+        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                && desktopVisibilityController != null
+                && desktopVisibilityController.areDesktopTasksVisible()
+                && !desktopVisibilityController.isRecentsGestureInProgress()) {
             // Return early to skip setting activity to appear as resumed
             // TODO: b/333533253 - Remove after flag rollout
             return;
@@ -1098,14 +1091,34 @@
         );
     }
 
-    public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
-        mTaskbarUIController = taskbarUIController;
+    @Override
+    public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
+        mTaskbarUIController = (LauncherTaskbarUIController) taskbarUIController;
     }
 
+    @Override
     public @Nullable LauncherTaskbarUIController getTaskbarUIController() {
         return mTaskbarUIController;
     }
 
+    /** Provides the translation X for the hotseat item. */
+    public int getHotseatItemTranslationX(ItemInfo itemInfo) {
+        int translationX = 0;
+        if (isBubbleBarEnabled()
+                && enableBubbleBarInPersistentTaskBar()
+                && mBubbleBarLocation != null) {
+            boolean isBubblesOnLeft = mBubbleBarLocation.isOnLeft(isRtl(getResources()));
+            translationX += mDeviceProfile
+                    .getHotseatTranslationXForNavBar(this, isBubblesOnLeft);
+        }
+        if (isBubbleBarEnabled()
+                && mDeviceProfile.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles())) {
+            translationX += (int) mDeviceProfile
+                    .getHotseatAdjustedTranslation(getContext(), itemInfo.cellX);
+        }
+        return translationX;
+    }
+
     public SplitToWorkspaceController getSplitToWorkspaceController() {
         return mSplitToWorkspaceController;
     }
@@ -1147,8 +1160,9 @@
     }
 
     @Nullable
+    @Override
     public DesktopVisibilityController getDesktopVisibilityController() {
-        return mDesktopVisibilityController;
+        return mTISBindHelper.getDesktopVisibilityController();
     }
 
     @Nullable
@@ -1183,7 +1197,8 @@
 
     @Override
     public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
-        ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(v);
+        ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(
+                v, item != null ? item : (ItemInfo) v.getTag());
         if (mLastTouchUpTime > 0) {
             activityOptions.options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_LAUNCHER,
                     mLastTouchUpTime);
@@ -1223,47 +1238,6 @@
         mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop);
     }
 
-    /**
-     * Return a new launch cookie for the activity launch if supported.
-     *
-     * @param info the item info for the launch
-     */
-    public IBinder getLaunchCookie(ItemInfo info) {
-        if (info == null) {
-            return null;
-        }
-        switch (info.container) {
-            case Favorites.CONTAINER_DESKTOP:
-            case Favorites.CONTAINER_HOTSEAT:
-            case Favorites.CONTAINER_PRIVATESPACE:
-                // Fall through and continue it's on the workspace (we don't support swiping back
-                // to other containers like all apps or the hotseat predictions (which can change)
-                break;
-            default:
-                if (info.container >= 0) {
-                    // Also allow swiping to folders
-                    break;
-                }
-                // Reset any existing launch cookies associated with the cookie
-                return ObjectWrapper.wrap(NO_MATCHING_ID);
-        }
-        switch (info.itemType) {
-            case Favorites.ITEM_TYPE_APPLICATION:
-            case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-            case Favorites.ITEM_TYPE_APPWIDGET:
-                // Fall through and continue if it's an app, shortcut, or widget
-                break;
-            default:
-                // Reset any existing launch cookies associated with the cookie
-                return ObjectWrapper.wrap(NO_MATCHING_ID);
-        }
-        return ObjectWrapper.wrap(new Integer(info.id));
-    }
-
-    public void setHintUserWillBeActive() {
-        addActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
-    }
-
     @Override
     public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
         super.onDisplayInfoChanged(context, info, flags);
@@ -1340,10 +1314,15 @@
         mTISBindHelper.setPredictiveBackToHomeInProgress(isInProgress);
     }
 
+    public boolean getPredictiveBackToHomeInProgress() {
+        return mIsPredictiveBackToHomeInProgress;
+    }
+
     @Override
     public boolean areDesktopTasksVisible() {
-        if (mDesktopVisibilityController != null) {
-            return mDesktopVisibilityController.areDesktopTasksVisible();
+        DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
+        if (desktopVisibilityController != null) {
+            return desktopVisibilityController.areDesktopTasksVisible();
         }
         return false;
     }
@@ -1371,10 +1350,11 @@
      */
     public void launchSplitTasks(
             @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
-        // Top/left and bottom/right tasks respectively.
-        Task task1 = groupTask.task1;
+        // SplitBounds can be null if coming from Taskbar launch.
+        final boolean firstTaskIsLeftTopTask = isFirstTaskLeftTopTask(groupTask);
         // task2 should never be null when calling this method. Allow a crash to catch invalid calls
-        Task task2 = groupTask.task2;
+        Task task1 = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
+        Task task2 = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
         mSplitSelectStateController.launchExistingSplitPair(
                 null /* launchingTaskView */,
                 task1.key.id,
@@ -1383,17 +1363,28 @@
                 /* callback= */ success -> mSplitSelectStateController.resetState(),
                 /* freezeTaskList= */ false,
                 groupTask.mSplitBounds == null
-                        ? SNAP_TO_50_50
+                        ? SNAP_TO_2_50_50
                         : groupTask.mSplitBounds.snapPosition,
                 remoteTransition);
     }
 
+    private static boolean isFirstTaskLeftTopTask(@NonNull GroupTask groupTask) {
+        return groupTask.mSplitBounds == null
+                || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
+    }
+
     /**
      * Launches two apps as an app pair.
      */
     public void launchAppPair(AppPairIcon appPairIcon) {
+        // Potentially show the Taskbar education once the app pair launch finishes
         mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon,
-                CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE);
+                CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
+                (success) -> {
+                    if (success && mTaskbarUIController != null) {
+                        mTaskbarUIController.showEduOnAppLaunch();
+                    }
+                });
     }
 
     public boolean canStartHomeSafely() {
@@ -1412,6 +1403,7 @@
     }
 
     @NonNull
+    @Override
     public TISBindHelper getTISBindHelper() {
         return mTISBindHelper;
     }
@@ -1425,6 +1417,23 @@
         return true;
     }
 
+    @Override
+    public void showShortcutBubble(ShortcutInfo info) {
+        if (info == null) return;
+        SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
+    }
+
+    @Override
+    public void showAppBubble(Intent intent) {
+        if (intent == null || intent.getPackage() == null) return;
+        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> {
 
@@ -1494,4 +1503,12 @@
     public boolean isRecentsViewVisible() {
         return getStateManager().getState().isRecentsViewVisible;
     }
+
+    public boolean isCanShowAllAppsEducationView() {
+        return mCanShowAllAppsEducationView;
+    }
+
+    public void setCanShowAllAppsEducationView(boolean canShowAllAppsEducationView) {
+        mCanShowAllAppsEducationView = canShowAllAppsEducationView;
+    }
 }
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 fa80dc2..d387794 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -26,7 +26,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.util.BaseDepthController;
@@ -119,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
@@ -134,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.
@@ -155,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;
             }
@@ -165,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,19 +201,8 @@
     }
 
     @Override
-    public float[] getOverviewScaleAndOffset(Launcher launcher) {
-        if (!FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()) {
-            return super.getOverviewScaleAndOffset(launcher);
-        }
-        // This handles the case of returning to the previous app from Overview -> All Apps gesture.
-        // This is the start scale/offset of overview that will be used for that transition.
-        // TODO (b/283336332): Translate in Y direction (ideally with overview resistance).
-        return new float[] {0.5f /* scale */, NO_OFFSET};
-    }
-
-    @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 2625646..ca388c6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -15,9 +15,9 @@
  */
 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;
-import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 
 import android.content.Context;
 import android.graphics.Color;
@@ -51,9 +51,11 @@
             return super.getVerticalProgress(launcher);
         }
         RecentsView recentsView = launcher.getOverviewPanel();
-        int transitionLength = LayoutUtils.getShelfTrackingDistance(launcher,
+        int transitionLength = LayoutUtils.getShelfTrackingDistance(
+                launcher,
                 launcher.getDeviceProfile(),
-                recentsView.getPagedOrientationHandler());
+                recentsView.getPagedOrientationHandler(),
+                recentsView.getSizeStrategy());
         AllAppsTransitionController controller = launcher.getAllAppsController();
         float scrollRange = Math.max(controller.getShiftRange(), 1);
         float progressDelta = (transitionLength / scrollRange);
@@ -89,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
@@ -107,8 +114,7 @@
 
     @Override
     public boolean isTaskbarAlignedWithHotseat(Launcher launcher) {
-        if (ENABLE_SHELL_TRANSITIONS) return false;
-        return super.isTaskbarAlignedWithHotseat(launcher);
+        return false;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 6822f1b..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;
@@ -209,7 +214,7 @@
         TaskView taskView = recentsView.getRunningTaskView();
         if (taskView != null) {
             if (recentsView.isTaskViewFullyVisible(taskView)) {
-                taskView.launchTasks();
+                taskView.launchWithAnimation();
             } else {
                 recentsView.snapToPage(recentsView.indexOfChild(taskView));
             }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 11e0ed5..1d9e492 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.DisplayController;
@@ -53,6 +54,7 @@
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.contextualeducation.GestureType;
 
 import java.util.function.BiConsumer;
 
@@ -219,6 +221,8 @@
             }
             if (mStartState != mEndState) {
                 logHomeGesture();
+                ContextualEduStatsManager.INSTANCE.get(mLauncher).updateEduStats(
+                        mSwipeDetector.isTrackpadGesture(), GestureType.HOME);
             }
             AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher);
             if (topOpenView != null) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index 3325009..ff726e6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -39,11 +39,9 @@
 import android.view.ViewConfiguration;
 
 import com.android.internal.jank.Cuj;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -88,13 +86,17 @@
     // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
     private ObjectAnimator mNormalToHintOverviewScrimAnimator;
 
+    private final QuickstepLauncher mLauncher;
+    private boolean mIsTrackpadSwipe;
+
     /**
      * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled.
      *                            Animation should be added to the provided AnimatorSet
      */
-    public NoButtonNavbarToOverviewTouchController(Launcher l,
+    public NoButtonNavbarToOverviewTouchController(QuickstepLauncher l,
             BiConsumer<AnimatorSet, Long> cancelSplitRunnable) {
         super(l);
+        mLauncher = l;
         mRecentsView = l.getOverviewPanel();
         mMotionPauseDetector = new MotionPauseDetector(l);
         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
@@ -104,7 +106,9 @@
 
     @Override
     protected boolean canInterceptTouch(MotionEvent ev) {
-        if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher)
+        mIsTrackpadSwipe = isTrackpadMotionEvent(ev);
+        mLauncher.setCanShowAllAppsEducationView(!mIsTrackpadSwipe);
+        if (!mIsTrackpadSwipe && DisplayController.getNavigationMode(mLauncher)
                 == THREE_BUTTONS) {
             return false;
         }
@@ -148,6 +152,7 @@
         super.onDragStart(start, startDisplacement);
 
         mMotionPauseDetector.clear();
+        mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe);
 
         if (handlingOverviewAnim()) {
             InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS,
@@ -191,6 +196,7 @@
         }
 
         mMotionPauseDetector.clear();
+        mIsTrackpadSwipe = false;
         mNormalToHintOverviewScrimAnimator = null;
         if (mLauncher.isInState(OVERVIEW)) {
             // Normally we would cleanup the state based on mCurrentAnimation, but since we stop
@@ -214,11 +220,6 @@
             mCancelSplitRunnable.accept(animatorSet, duration);
             animatorSet.start();
         }
-        if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() &&
-                ((mFromState == NORMAL && mToState == ALL_APPS)
-                        || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) {
-            mVibratorWrapper.vibrateForDragBump();
-        }
     }
 
     private void onMotionPauseDetected() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index ab277b6..9164405 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -121,6 +121,7 @@
     private AnimatorPlaybackController mNonOverviewAnim;
     private AnimatorPlaybackController mXOverviewAnim;
     private AnimatedFloat mYOverviewAnim;
+    private boolean mIsTrackpadSwipe;
 
     public NoButtonQuickSwitchTouchController(QuickstepLauncher launcher) {
         mLauncher = launcher;
@@ -128,7 +129,10 @@
         mRecentsView = mLauncher.getOverviewPanel();
         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
         mYRange = LayoutUtils.getShelfTrackingDistance(
-            mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
+                mLauncher,
+                mLauncher.getDeviceProfile(),
+                mRecentsView.getPagedOrientationHandler(),
+                mRecentsView.getSizeStrategy());
         mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
@@ -177,7 +181,8 @@
             return false;
         }
         if (isTrackpadMultiFingerSwipe(ev)) {
-            return isTrackpadFourFingerSwipe(ev);
+            mIsTrackpadSwipe = isTrackpadFourFingerSwipe(ev);
+            return mIsTrackpadSwipe;
         }
         return true;
     }
@@ -185,6 +190,7 @@
     @Override
     public void onDragStart(boolean start) {
         mMotionPauseDetector.clear();
+        mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe);
         if (start) {
             InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
             InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index b5914a1..b562838 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -29,7 +29,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.AllAppsSwipeController;
@@ -93,9 +92,7 @@
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
         if (fromState == ALL_APPS && !isDragTowardPositive) {
-            return FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()
-                    ? mLauncher.getStateManager().getLastState()
-                    : NORMAL;
+            return NORMAL;
         } else if (fromState == NORMAL && shouldOpenAllApps(isDragTowardPositive)) {
             return ALL_APPS;
         }
@@ -145,8 +142,11 @@
                     .createPlaybackController();
             mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
             RecentsView recentsView = mLauncher.getOverviewPanel();
-            totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher,
-                    mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler());
+            totalShift = LayoutUtils.getShelfTrackingDistance(
+                    mLauncher,
+                    mLauncher.getDeviceProfile(),
+                    recentsView.getPagedOrientationHandler(),
+                    recentsView.getSizeStrategy());
         } else {
             mCurrentAnimation = mLauncher.getStateManager()
                     .createAnimationToNewWorkspace(mToState, config);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index d98e608..cb2c324 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -58,8 +58,6 @@
     /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/
     private boolean mCanIntercept;
 
-    private boolean mIsTrackpadReverseScroll;
-
     public StatusBarTouchController(Launcher l) {
         mLauncher = l;
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mLauncher);
@@ -95,8 +93,6 @@
             }
             mDownEvents.clear();
             mDownEvents.put(pid, new PointF(ev.getX(), ev.getY()));
-            mIsTrackpadReverseScroll = !mLauncher.isNaturalScrollingEnabled()
-                    && isTrackpadScroll(ev);
         } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
             // Check!! should only set it only when threshold is not entered.
             mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx)));
@@ -107,9 +103,6 @@
         if (action == ACTION_MOVE && mDownEvents.contains(pid)) {
             float dy = ev.getY(idx) - mDownEvents.get(pid).y;
             float dx = ev.getX(idx) - mDownEvents.get(pid).x;
-            if (mIsTrackpadReverseScroll) {
-                dy = -dy;
-            }
             // Currently input dispatcher will not do touch transfer if there are more than
             // one touch pointer. Hence, even if slope passed, only set the slippery flag
             // when there is single touch event. (context: InputDispatcher.cpp line 1445)
@@ -134,7 +127,6 @@
             mLauncher.getStatsLogManager().logger()
                     .log(LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN);
             setWindowSlippery(false);
-            mIsTrackpadReverseScroll = false;
             return true;
         }
         return true;
@@ -161,9 +153,9 @@
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
-        if (!mLauncher.isInState(LauncherState.NORMAL) ||
-                AbstractFloatingView.getTopOpenViewWithType(mLauncher,
-                        AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) {
+        if (isTrackpadScroll(ev) || !mLauncher.isInState(LauncherState.NORMAL)
+                || AbstractFloatingView.getTopOpenViewWithType(mLauncher,
+                AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) {
             return false;
         } else {
             // For NORMAL state, only listen if the event originated above the navbar height
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 3c7f335..202276e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -22,9 +22,9 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
+import android.graphics.Rect;
 import android.os.VibrationEffect;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.app.animation.Interpolators;
@@ -67,7 +67,7 @@
     protected final CONTAINER mContainer;
     private final SingleAxisSwipeDetector mDetector;
     private final RecentsView mRecentsView;
-    private final int[] mTempCords = new int[2];
+    private final Rect mTempRect = new Rect();
     private final boolean mIsRtl;
 
     private AnimatorPlaybackController mCurrentAnimation;
@@ -181,7 +181,7 @@
                         // - The task is snapped
                         mAllowGoingDown = i == mRecentsView.getCurrentPage()
                                 && DisplayController.getNavigationMode(mContainer).hasGestures
-                                && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask())
+                                && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isLargeTile())
                                 && mRecentsView.isTaskInExpectedScrollPosition(i);
 
                         directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection;
@@ -252,10 +252,8 @@
                     mTaskBeingDragged, maxDuration, currentInterpolator);
 
             // Since the thumbnail is what is filling the screen, based the end displacement on it.
-            View thumbnailView = mTaskBeingDragged.getFirstSnapshotView();
-            mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
-            dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
-            mEndDisplacement = secondaryLayerDimension - mTempCords[1];
+            mTaskBeingDragged.getThumbnailBounds(mTempRect, /*relativeToDragLayer=*/true);
+            mEndDisplacement = secondaryLayerDimension - mTempRect.bottom;
         }
         mEndDisplacement *= verticalFactor;
         mCurrentAnimation = pa.createPlaybackController();
@@ -312,7 +310,7 @@
                 // Set mOverrideVelocity to control task dismiss velocity in onDragEnd
                 int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity;
                 if (mRecentsView.showAsGrid()) {
-                    if (mTaskBeingDragged.isFocusedTask()) {
+                    if (mTaskBeingDragged.isLargeTile()) {
                         velocityDimenId =
                                 R.dimen.default_task_dismiss_drag_velocity_grid_focus_task;
                     } else {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index f020c8f..97d7179 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -45,7 +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.GestureState.GestureEndTarget.ALL_APPS;
+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;
@@ -53,13 +53,10 @@
 import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
 import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELED;
+import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
@@ -91,11 +88,13 @@
 import android.view.WindowInsets;
 import android.view.animation.Interpolator;
 import android.widget.Toast;
+import android.window.DesktopModeFlags;
 import android.window.PictureInPictureSurfaceTransaction;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.Cuj;
 import com.android.internal.util.LatencyTracker;
@@ -105,10 +104,12 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
 import com.android.launcher3.dragndrop.DragView;
 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;
@@ -117,13 +118,14 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.WindowBounds;
-import com.android.quickstep.BaseActivityInterface.AnimationFactory;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.ContextInitListener;
 import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.InputProxyHandlerFactory;
 import com.android.quickstep.util.MotionPauseDetector;
@@ -140,6 +142,7 @@
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.contextualeducation.GestureType;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -148,9 +151,9 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
-import com.android.window.flags.Flags;
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
+import com.android.wm.shell.shared.TransactionPool;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -166,8 +169,9 @@
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
-public abstract class AbsSwipeUpHandler<T extends RecentsViewContainer,
-        Q extends RecentsView, S extends BaseState<S>>
+public abstract class AbsSwipeUpHandler<
+        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";
@@ -177,26 +181,25 @@
     // Fraction of the scroll and transform animation in which the current task fades out
     private static final float KQS_TASK_FADE_ANIMATION_FRACTION = 0.4f;
 
-    protected final BaseContainerInterface<S, T> mContainerInterface;
+    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;
 
     // Null if the recents animation hasn't started yet or has been canceled or finished.
     protected @Nullable RecentsAnimationController mRecentsAnimationController;
-    protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
-    protected @Nullable T mContainer;
-    protected @Nullable Q mRecentsView;
+    protected @Nullable RECENTS_CONTAINER mContainer;
+    protected @Nullable RECENTS_VIEW mRecentsView;
     protected Runnable mGestureEndCallback;
     protected MultiStateCallback mStateCallback;
     protected boolean mCanceled;
     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);
@@ -233,6 +236,8 @@
             getNextStateFlag("STATE_SCALED_CONTROLLER_HOME");
     private static final int STATE_SCALED_CONTROLLER_RECENTS =
             getNextStateFlag("STATE_SCALED_CONTROLLER_RECENTS");
+    private static final int STATE_PARALLEL_ANIM_FINISHED =
+            getNextStateFlag("STATE_PARALLEL_ANIM_FINISHED");
 
     protected static final int STATE_HANDLER_INVALIDATED =
             getNextStateFlag("STATE_HANDLER_INVALIDATED");
@@ -258,8 +263,6 @@
             getNextStateFlag("STATE_CURRENT_TASK_FINISHED");
     private static final int STATE_FINISH_WITH_NO_END =
             getNextStateFlag("STATE_FINISH_WITH_NO_END");
-    private static final int STATE_SETTLED_ON_ALL_APPS =
-            getNextStateFlag("STATE_SETTLED_ON_ALL_APPS");
 
     private static final int LAUNCHER_UI_STATES =
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED |
@@ -289,7 +292,7 @@
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
     protected final TaskAnimationManager mTaskAnimationManager;
-
+    protected final RecentsWindowManager mRecentsWindowManager;
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim[] mRunningWindowAnim;
     // Possible second animation running at the same time as mRunningWindowAnim
@@ -313,7 +316,6 @@
     private boolean mGestureStarted;
     private boolean mLogDirectionUpOrLeft = true;
     private boolean mIsLikelyToStartNewTask;
-    private boolean mIsInAllAppsRegion;
 
     private final long mTouchTimeMs;
     private long mLauncherFrameDrawnTime;
@@ -351,10 +353,10 @@
     public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
-            InputConsumerController inputConsumer) {
+            InputConsumerController inputConsumer, RecentsWindowManager recentsWindowManager) {
         super(context, deviceState, gestureState);
         mContainerInterface = gestureState.getContainerInterface();
-        mActivityInitListener =
+        mContextInitListener =
                 mContainerInterface.createActivityInitListener(this::onActivityInit);
         mInputConsumerProxy =
                 new InputConsumerProxy(context, /* rotationSupplier = */ () -> {
@@ -367,6 +369,7 @@
                     endLauncherTransitionController();
                 }, new InputProxyHandlerFactory(mContainerInterface, mGestureState));
         mTaskAnimationManager = taskAnimationManager;
+        mRecentsWindowManager = recentsWindowManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
 
@@ -448,11 +451,9 @@
         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_SETTLED_ON_ALL_APPS | STATE_SCREENSHOT_CAPTURED
-                        | STATE_GESTURE_COMPLETED,
-                this::finishCurrentTransitionToAllApps);
 
         mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
@@ -465,6 +466,8 @@
         mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED
                         | STATE_RECENTS_SCROLLING_FINISHED,
                 this::onSettledOnEndTarget);
+        mGestureState.runOnceAtState(STATE_END_TARGET_SET | STATE_RECENTS_ANIMATION_STARTED,
+                this::onCalculateEndTarget);
 
         mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
         mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
@@ -475,16 +478,16 @@
                 this::resetStateForAnimationCancel);
     }
 
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
+    protected boolean onActivityInit(Boolean isHomeStarted) {
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return false;
         }
 
-        T createdContainer = (T) mContainerInterface.getCreatedContainer();
+        RECENTS_CONTAINER createdContainer = mContainerInterface.getCreatedContainer();
         if (createdContainer != null) {
             initTransitionEndpoints(createdContainer.getDeviceProfile());
         }
-        final T container = (T) mContainerInterface.getCreatedContainer();
+        final RECENTS_CONTAINER container = mContainerInterface.getCreatedContainer();
         if (mContainer == container) {
             return true;
         }
@@ -503,11 +506,11 @@
             initStateCallbacks();
             mStateCallback.setState(oldState);
         }
-        mWasLauncherAlreadyVisible = alreadyOnHome;
+        mWasLauncherAlreadyVisible = isHomeStarted;
         mContainer = container;
         // Override the visibility of the activity until the gesture actually starts and we swipe
         // up, or until we transition home and the home animation is composed
-        if (alreadyOnHome) {
+        if (isHomeStarted) {
             mContainer.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
         } else {
             mContainer.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
@@ -517,7 +520,7 @@
         mRecentsView.setOnPageTransitionEndCallback(null);
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
-        if (alreadyOnHome) {
+        if (isHomeStarted) {
             onLauncherStart();
         } else {
             container.addEventCallback(EVENT_STARTED, mLauncherOnStartCallback);
@@ -531,14 +534,7 @@
             HashMap<Integer, ThumbnailData> snapshots =
                     mGestureState.consumeRecentsAnimationCanceledSnapshot();
             if (snapshots != null) {
-                mRecentsView.switchToScreenshot(snapshots, () -> {
-                    if (mRecentsAnimationController != null) {
-                        mRecentsAnimationController.cleanupScreenshot();
-                    } else if (mDeferredCleanupRecentsAnimationController != null) {
-                        mDeferredCleanupRecentsAnimationController.cleanupScreenshot();
-                        mDeferredCleanupRecentsAnimationController = null;
-                    }
-                });
+                mRecentsView.switchToScreenshot(snapshots, () -> {});
                 mRecentsView.onRecentsAnimationComplete();
             }
         });
@@ -558,7 +554,7 @@
     }
 
     private void onLauncherStart() {
-        final T container = (T) mContainerInterface.getCreatedContainer();
+        final RECENTS_CONTAINER container = mContainerInterface.getCreatedContainer();
         if (container == null || mContainer != container) {
             return;
         }
@@ -715,9 +711,7 @@
                 maybeUpdateRecentsAttachedState(true/* animate */, true/* moveRunningTask */);
                 Optional.ofNullable(mContainerInterface.getTaskbarController())
                         .ifPresent(TaskbarUIController::startTranslationSpring);
-                if (!mIsInAllAppsRegion) {
-                    performHapticFeedback();
-                }
+                performHapticFeedback();
             }
 
             @Override
@@ -728,11 +722,18 @@
     }
 
     private void maybeUpdateRecentsAttachedState() {
-        maybeUpdateRecentsAttachedState(true /* animate */);
+        maybeUpdateRecentsAttachedState(/* animate= */ true);
     }
 
     protected void maybeUpdateRecentsAttachedState(boolean animate) {
-        maybeUpdateRecentsAttachedState(animate, false /* moveRunningTask */);
+        maybeUpdateRecentsAttachedState(animate, /* moveRunningTask= */ false);
+    }
+
+    protected void maybeUpdateRecentsAttachedState(boolean animate, boolean moveRunningTask) {
+        maybeUpdateRecentsAttachedState(
+                animate,
+                moveRunningTask,
+                mRecentsView != null && mRecentsView.shouldUpdateRunningTaskAlpha());
     }
 
     /**
@@ -743,8 +744,10 @@
      * Note this method has no effect unless the navigation mode is NO_BUTTON.
      * @param animate whether to animate when attaching RecentsView
      * @param moveRunningTask whether to move running task to front when attaching
+     * @param updateRunningTaskAlpha Whether to update the running task's attached alpha
      */
-    private void maybeUpdateRecentsAttachedState(boolean animate, boolean moveRunningTask) {
+    private void maybeUpdateRecentsAttachedState(
+            boolean animate, boolean moveRunningTask, boolean updateRunningTaskAlpha) {
         if ((!mDeviceState.isFullyGesturalNavMode() && !mGestureState.isTrackpadGesture())
                 || mRecentsView == null) {
             return;
@@ -756,9 +759,7 @@
                 .findTask(mGestureState.getTopRunningTaskId())
                 : null;
         final boolean recentsAttachedToAppWindow;
-        if (mIsInAllAppsRegion) {
-            recentsAttachedToAppWindow = false;
-        } else if (mGestureState.getEndTarget() != null) {
+        if (mGestureState.getEndTarget() != null) {
             recentsAttachedToAppWindow = mGestureState.getEndTarget().recentsAttachedToAppWindow;
         } else if (mContinuingLastGesture
                 && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
@@ -775,7 +776,8 @@
             // TaskView jumping to new position as we move the tasks.
             mRecentsView.moveRunningTaskToFront();
         }
-        mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+        mAnimationFactory.setRecentsAttachedToAppWindow(
+                recentsAttachedToAppWindow, animate, updateRunningTaskAlpha);
 
         // Reapply window transform throughout the attach animation, as the animation affects how
         // much the window is bound by overscroll (vs moving freely).
@@ -814,37 +816,12 @@
         }
     }
 
-    /**
-     * Update whether user is currently dragging in a region that will trigger all apps.
-     */
-    private void setIsInAllAppsRegion(boolean isInAllAppsRegion) {
-        if (mIsInAllAppsRegion == isInAllAppsRegion
-                || !mContainerInterface.allowAllAppsFromOverview()) {
-            return;
-        }
-        mIsInAllAppsRegion = isInAllAppsRegion;
-
-        // Newly entering or exiting the zone - do haptic and animate recent tasks.
-        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
-        maybeUpdateRecentsAttachedState(true);
-
-        if (mContainer != null) {
-            mContainer.getAppsView().getSearchUiManager()
-                    .prepareToFocusEditText(mIsInAllAppsRegion);
-        }
-
-        // Draw active task below Launcher so that All Apps can appear over it.
-        runActionOnRemoteHandles(remoteTargetHandle ->
-                remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(isInAllAppsRegion));
-    }
-
-
     private void buildAnimationController() {
         if (!canCreateNewOrUpdateExistingLauncherTransitionController()) {
             return;
         }
         initTransitionEndpoints(mContainer.getDeviceProfile());
-        mAnimationFactory.createActivityInterface(mTransitionDragLength);
+        mAnimationFactory.createContainerInterface(mTransitionDragLength);
     }
 
     /**
@@ -888,18 +865,19 @@
         }
     }
 
+    public Intent getHomeIntent() {
+        return mGestureState.getHomeIntent();
+    }
+
     public Intent getLaunchIntent() {
         return mGestureState.getOverviewIntent();
     }
-
     /**
      * Called when the value of {@link #mCurrentShift} changes
      */
     @UiThread
     @Override
     public void onCurrentShiftUpdated() {
-        float threshold = DeviceConfigWrapper.get().getAllAppsOverviewThreshold() / 100f;
-        setIsInAllAppsRegion(mCurrentShift.value >= threshold);
         updateSysUiFlags(mCurrentShift.value);
         applyScrollAndTransform();
 
@@ -933,7 +911,6 @@
             // We will handle the sysui flags based on the centermost task view.
             mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed
                     ||  (quickswitchThresholdPassed && centermostTaskFlags != 0));
-            mRecentsAnimationController.setSplitScreenMinimized(mContext, swipeUpThresholdPassed);
             // Provide a hint to WM the direction that we will be settling in case the animation
             // needs to be canceled
             mRecentsAnimationController.setWillFinishToHome(swipeUpThresholdPassed);
@@ -952,7 +929,7 @@
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
         super.onRecentsAnimationStart(controller, targets);
-        if (targets.hasDesktopTasks()) {
+        if (targets.hasDesktopTasks(mContext)) {
             mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets);
         } else {
             int untrimmedAppCount = mRemoteTargetHandles.length;
@@ -988,7 +965,6 @@
                 dp = dp.copy(mContext);
             }
             dp.updateInsets(targets.homeContentInsets);
-            dp.updateIsSeascape(mContext);
             initTransitionEndpoints(dp);
             orientationState.setMultiWindowMode(dp.isMultiWindowMode);
         }
@@ -1004,13 +980,8 @@
 
     @Override
     public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "cancelRecentsAnimation",
-                /* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
-        mActivityInitListener.unregister("AbsSwipeUpHandler.onRecentsAnimationCanceled");
-        // Cache the recents animation controller so we can defer its cleanup to after having
-        // properly cleaned up the screenshot without accidentally using it.
-        mDeferredCleanupRecentsAnimationController = mRecentsAnimationController;
+        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;
@@ -1082,7 +1053,7 @@
      */
     @UiThread
     private void notifyGestureStarted() {
-        final T curActivity = mContainer;
+        final RECENTS_CONTAINER curActivity = mContainer;
         if (curActivity != null) {
             // Once the gesture starts, we can no longer transition home through the button, so
             // reset the force override of the activity visibility
@@ -1150,7 +1121,25 @@
         }
     }
 
-    private void onSettledOnEndTarget() {
+    /**
+     * Called if the end target has been set and the recents animation is started.
+     */
+    @VisibleForTesting
+    protected void onCalculateEndTarget() {
+        final GestureEndTarget endTarget = mGestureState.getEndTarget();
+
+        switch (endTarget) {
+            case HOME:
+                // Early detach the nav bar if endTarget is determined as HOME
+                if (mRecentsAnimationController != null) {
+                    mRecentsAnimationController.detachNavigationBarFromApp(true);
+                }
+                break;
+        }
+    }
+
+    @VisibleForTesting
+    protected void onSettledOnEndTarget() {
         // Fast-finish the attaching animation if it's still running.
         maybeUpdateRecentsAttachedState(false);
         final GestureEndTarget endTarget = mGestureState.getEndTarget();
@@ -1159,18 +1148,21 @@
 
         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) {
-            case ALL_APPS:
-                mStateCallback.setState(STATE_SETTLED_ON_ALL_APPS | STATE_CAPTURE_SCREENSHOT);
-                break;
             case HOME:
                 mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
                 // Notify the SysUI to use fade-in animation when entering PiP
@@ -1198,10 +1190,7 @@
             // Resets this value as the gesture is now complete.
             mContainerInterface.getTaskbarController().setUserIsNotGoingHome(false);
         }
-        ActiveGestureLog.INSTANCE.addLog(
-                new ActiveGestureLog.CompoundString("onSettledOnEndTarget ")
-                        .append(endTarget.name()),
-                /* gestureEvent= */ ON_SETTLED_ON_END_TARGET);
+        ActiveGestureProtoLogProxy.logOnSettledOnEndTarget(endTarget.name());
     }
 
     /** @return Whether this was the task we were waiting to appear, and thus handled it. */
@@ -1217,11 +1206,9 @@
                 failureReason.append("STATE_START_NEW_TASK was never set");
             } else {
                 TaskInfo taskInfo = appearedTaskTargets[0].taskInfo;
-                failureReason.append("Unexpected task appeared")
-                                .append(" id=")
-                                .append(taskInfo.taskId)
-                                .append(" pkg=")
-                                .append(taskInfo.baseIntent.getComponent().getPackageName());
+                failureReason.append("Unexpected task appeared id=%d, pkg=%s",
+                        taskInfo.taskId,
+                        taskInfo.baseIntent.getComponent().getPackageName());
             }
             return false;
         }
@@ -1235,19 +1222,10 @@
 
     private GestureEndTarget calculateEndTarget(
             PointF velocityPxPerMs, float endVelocityPxPerMs, boolean isFlingY, boolean isCancel) {
-
-        ActiveGestureErrorDetector.GestureEvent gestureEvent =
-                velocityPxPerMs.x == 0 && velocityPxPerMs.y == 0
-                        ? INVALID_VELOCITY_ON_SWIPE_UP
-                        : null;
-        ActiveGestureLog.INSTANCE.addLog(
-                new ActiveGestureLog.CompoundString("calculateEndTarget: velocities=(x=")
-                        .append(dpiFromPx(velocityPxPerMs.x))
-                        .append("dp/ms, y=")
-                        .append(dpiFromPx(velocityPxPerMs.y))
-                        .append("dp/ms), angle=")
-                        .append(Math.toDegrees(Math.atan2(
-                                -velocityPxPerMs.y, velocityPxPerMs.x))), gestureEvent);
+        ActiveGestureProtoLogProxy.logOnCalculateEndTarget(
+                dpiFromPx(velocityPxPerMs.x),
+                dpiFromPx(velocityPxPerMs.y),
+                Math.toDegrees(Math.atan2(-velocityPxPerMs.y, velocityPxPerMs.x)));
 
         if (mGestureState.isHandlingAtomicEvent()) {
             // Button mode, this is only used to go to recents.
@@ -1272,9 +1250,9 @@
         TaskView currentPageTaskView = mRecentsView != null
                 ? mRecentsView.getCurrentPageTaskView() : null;
 
-        if (Flags.enableDesktopWindowingMode()
-                && !(Flags.enableDesktopWindowingWallpaperActivity()
-                && Flags.enableDesktopWindowingQuickSwitch())) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)
+                && !(DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH.isTrue())) {
             if ((nextPageTaskView instanceof DesktopTaskView
                     || currentPageTaskView instanceof DesktopTaskView)
                     && endTarget == NEW_TASK) {
@@ -1289,9 +1267,6 @@
         final boolean willGoToNewTask =
                 isScrollingToNewTask() && Math.abs(velocity.x) > Math.abs(endVelocity);
         final boolean isSwipeUp = endVelocity < 0;
-        if (mIsInAllAppsRegion) {
-            return isSwipeUp ? ALL_APPS : LAST_TASK;
-        }
         if (!isSwipeUp) {
             final boolean isCenteredOnNewTask = mRecentsView != null
                     && mRecentsView.getDestinationPage() != mRecentsView.getRunningTaskIndex();
@@ -1307,9 +1282,7 @@
         // Fully gestural mode.
         final boolean isFlingX = Math.abs(velocity.x) > mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_speed);
-        if (mIsInAllAppsRegion) {
-            return ALL_APPS;
-        } else if (isScrollingToNewTask && isFlingX) {
+        if (isScrollingToNewTask && isFlingX) {
             // Flinging towards new task takes precedence over mIsMotionPaused (which only
             // checks y-velocity).
             return NEW_TASK;
@@ -1364,8 +1337,7 @@
                     .setUserIsNotGoingHome(endTarget != GestureState.GestureEndTarget.HOME);
         }
 
-        float endShift = endTarget == ALL_APPS ? mDragLengthFactor
-                : endTarget.isLauncher ? 1 : 0;
+        float endShift = endTarget.isLauncher ? 1 : 0;
         final float startShift;
         if (!isFling) {
             long expectedDuration = Math.abs(Math.round((endShift - currentShift)
@@ -1386,7 +1358,7 @@
             }
         }
         Interpolator interpolator;
-        S state = mContainerInterface.stateFromGestureEndTarget(endTarget);
+        STATE state = mContainerInterface.stateFromGestureEndTarget(endTarget);
         if (isKeyboardTaskFocusPending()) {
             interpolator = EMPHASIZED;
         } else if (state.displayOverviewTasksAsGrid(mDp)) {
@@ -1404,10 +1376,8 @@
             duration = mContainer != null && mContainer.getDeviceProfile().isTaskbarPresent
                     ? StaggeredWorkspaceAnim.DURATION_TASKBAR_MS
                     : StaggeredWorkspaceAnim.DURATION_MS;
-            // Early detach the nav bar once the endTarget is determined as HOME
-            if (mRecentsAnimationController != null) {
-                mRecentsAnimationController.detachNavigationBarFromApp(true);
-            }
+            ContextualEduStatsManager.INSTANCE.get(mContext).updateEduStats(
+                    mGestureState.isTrackpadGesture(), GestureType.HOME);
         } else if (endTarget == RECENTS) {
             if (mRecentsView != null) {
                 int nearestPage = mRecentsView.getDestinationPage();
@@ -1432,6 +1402,8 @@
                 if (!mGestureState.isHandlingAtomicEvent() || isScrolling) {
                     duration = Math.max(duration, mRecentsView.getScroller().getDuration());
                 }
+                ContextualEduStatsManager.INSTANCE.get(mContext).updateEduStats(
+                        mGestureState.isTrackpadGesture(), GestureType.OVERVIEW);
             }
         } else if (endTarget == LAST_TASK && mRecentsView != null
                 && mRecentsView.getNextPage() != mRecentsView.getRunningTaskIndex()) {
@@ -1445,9 +1417,9 @@
             setClampScrollOffset(false);
         };
 
-        if (Flags.enableDesktopWindowingMode()
-                && !(Flags.enableDesktopWindowingWallpaperActivity()
-                && Flags.enableDesktopWindowingQuickSwitch())) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)
+                && !(DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH.isTrue())) {
             if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
                     && !(mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView))) {
                 ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
@@ -1571,9 +1543,12 @@
                     @Override
                     public void onAnimationEnd(Animator animation) {
                         mParallelRunningAnim = null;
+                        mStateCallback.setStateOnUiThread(STATE_PARALLEL_ANIM_FINISHED);
                     }
                 });
                 mParallelRunningAnim.start();
+            } else {
+                mStateCallback.setStateOnUiThread(STATE_PARALLEL_ANIM_FINISHED);
             }
         }
 
@@ -1631,14 +1606,17 @@
                             mRecentsAnimationController.screenshotTask(taskId));
                 });
 
-                // let SystemUi reparent the overlay leash as soon as possible
+                // let SystemUi reparent the overlay leash as soon as possible;
+                // make sure to pass in an empty src-rect-hint if overlay is present, since we
+                // use our own calculated source-rect-hint for the animation.
                 SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
                         mSwipePipToHomeAnimator.getTaskId(),
                         mSwipePipToHomeAnimator.getComponentName(),
                         mSwipePipToHomeAnimator.getDestinationBounds(),
                         mSwipePipToHomeAnimator.getContentOverlay(),
                         mSwipePipToHomeAnimator.getAppBounds(),
-                        mSwipePipToHomeAnimator.getSourceRectHint());
+                        mSwipePipToHomeAnimator.getContentOverlay() != null ? new Rect()
+                                : mSwipePipToHomeAnimator.getSourceRectHint());
 
                 windowAnim = mSwipePipToHomeAnimators;
             } else {
@@ -1733,8 +1711,7 @@
 
     private int calculateWindowRotation(RemoteAnimationTarget runningTaskTarget,
             RecentsOrientedState orientationState) {
-        if (runningTaskTarget.rotationChange != 0
-                && TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
+        if (runningTaskTarget.rotationChange != 0) {
             return Math.abs(runningTaskTarget.rotationChange) == ROTATION_90
                     ? ROTATION_270 : ROTATION_90;
         } else {
@@ -1990,12 +1967,6 @@
         reset();
     }
 
-    @UiThread
-    private void finishCurrentTransitionToAllApps() {
-        finishCurrentTransitionToHome();
-        reset();
-    }
-
     private void reset() {
         mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
         if (mContainer != null) {
@@ -2008,15 +1979,13 @@
      * handler (in case of quick switch).
      */
     private void cancelCurrentAnimation() {
-        ActiveGestureLog.INSTANCE.addLog(
-                "AbsSwipeUpHandler.cancelCurrentAnimation",
-                ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION);
+        ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerCancelCurrentAnimation();
         mCanceled = true;
         mCurrentShift.cancelAnimation();
 
         // Cleanup when switching handlers
         mInputConsumerProxy.unregisterOnTouchDownCallback();
-        mActivityInitListener.unregister("AbsSwipeUpHandler.cancelCurrentAnimation");
+        mContextInitListener.unregister("AbsSwipeUpHandler.cancelCurrentAnimation");
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                 mActivityRestartListener);
         mTaskSnapshotCache.clear();
@@ -2034,7 +2003,7 @@
             mGestureEndCallback.run();
         }
 
-        mActivityInitListener.unregister("AbsSwipeUpHandler.invalidateHandler");
+        mContextInitListener.unregister("AbsSwipeUpHandler.invalidateHandler");
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                 mActivityRestartListener);
         mTaskSnapshotCache.clear();
@@ -2096,7 +2065,6 @@
             // If there are no targets, then we don't need to capture anything
             mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         } else {
-            boolean finishTransitionPosted = false;
             // If we already have cached screenshot(s) from running tasks, skip update
             boolean shouldUpdate = false;
             int[] runningTaskIds = mGestureState.getRunningTaskIds(mIsSwipeForSplit);
@@ -2120,45 +2088,31 @@
                         }
 
                         MAIN_EXECUTOR.execute(() -> {
-                            if (!updateThumbnail(false /* refreshView */)) {
-                                setScreenshotCapturedState();
-                            }
+                            updateThumbnail();
+                            setScreenshotCapturedState();
                         });
                     });
                     return;
                 }
 
-                finishTransitionPosted = updateThumbnail(false /* refreshView */);
+                updateThumbnail();
             }
 
-            if (!finishTransitionPosted) {
-                setScreenshotCapturedState();
-            }
+            setScreenshotCapturedState();
         }
     }
 
     // Returns whether finish transition was posted.
-    private boolean updateThumbnail(boolean refreshView) {
+    private void updateThumbnail() {
         if (mGestureState.getEndTarget() == HOME
                 || mGestureState.getEndTarget() == NEW_TASK
-                || mGestureState.getEndTarget() == ALL_APPS
                 || mRecentsView == null) {
             // Capture the screenshot before finishing the transition to home or quickswitching to
             // ensure it's taken in the correct orientation, but no need to update the thumbnail.
-            return false;
+            return;
         }
 
-        boolean finishTransitionPosted = false;
-        TaskView updatedTaskView = mRecentsView.updateThumbnail(mTaskSnapshotCache, refreshView);
-        if (updatedTaskView != null && refreshView && !mCanceled) {
-            // Defer finishing the animation until the next launcher frame with the
-            // new thumbnail
-            finishTransitionPosted = ViewUtils.postFrameDrawn(updatedTaskView,
-                    () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
-                    this::isCanceled);
-        }
-
-        return finishTransitionPosted;
+        mRecentsView.updateThumbnail(mTaskSnapshotCache);
     }
 
     private void setScreenshotCapturedState() {
@@ -2290,9 +2244,9 @@
                     mRecentsAnimationController, mRecentsAnimationTargets);
         });
 
-        if (Flags.enableDesktopWindowingMode()
-                && !(Flags.enableDesktopWindowingWallpaperActivity()
-                        && Flags.enableDesktopWindowingQuickSwitch())) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)
+                && !(DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                        && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH.isTrue())) {
             if (mRecentsView.getNextPageTaskView() instanceof DesktopTaskView
                     || mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView) {
                 mRecentsViewScrollLinked = false;
@@ -2326,18 +2280,15 @@
             TaskView nextTask = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
             if (nextTask != null) {
                 int[] taskIds = nextTask.getTaskIds();
-                ActiveGestureLog.CompoundString nextTaskLog = new ActiveGestureLog.CompoundString(
-                        "Launching task: ");
+                ActiveGestureLog.CompoundString nextTaskLog =
+                        ActiveGestureLog.CompoundString.newEmptyString();
                 for (TaskContainer container : nextTask.getTaskContainers()) {
                     if (container == null) {
                         continue;
                     }
-                    nextTaskLog
-                            .append("[id: ")
-                            .append(container.getTask().key.id)
-                            .append(", pkg: ")
-                            .append(container.getTask().key.getPackageName())
-                            .append("] | ");
+                    nextTaskLog.append("[id: %d, pkg: %s] | ",
+                            container.getTask().key.id,
+                            container.getTask().key.getPackageName());
                 }
                 mGestureState.updateLastStartedTaskIds(taskIds);
                 boolean hasTaskPreviouslyAppeared = Arrays.stream(taskIds).anyMatch(
@@ -2346,8 +2297,8 @@
                 if (!hasTaskPreviouslyAppeared) {
                     ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
                 }
-                ActiveGestureLog.INSTANCE.addLog(nextTaskLog);
-                nextTask.launchTask(success -> {
+                ActiveGestureProtoLogProxy.logStartNewTask(nextTaskLog);
+                nextTask.launchWithoutAnimation(true, success -> {
                     resultCallback.accept(success);
                     if (success) {
                         if (hasTaskPreviouslyAppeared) {
@@ -2360,7 +2311,7 @@
                         }
                     }
                     return Unit.INSTANCE;
-                }, true /* freezeTaskList */);
+                }  /* freezeTaskList */);
             } else {
                 mContainerInterface.onLaunchTaskFailed();
                 Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
@@ -2424,31 +2375,28 @@
             return;
         }
         final Runnable onFinishComplete = () -> {
-            ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                    "AbsSwipeUpHandler.onTasksAppeared: ")
-                    .append("force finish recents animation complete; clearing state callback."));
+            ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerOnTasksAppeared();
             mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
         };
-        ActiveGestureLog.CompoundString forceFinishReason = new ActiveGestureLog.CompoundString(
-                "Forcefully finishing recents animation: ");
+        ActiveGestureLog.CompoundString forceFinishReason =
+                ActiveGestureLog.CompoundString.newEmptyString();
         if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED)
                 && !hasStartedTaskBefore(appearedTaskTargets)) {
             // This is a special case, if a task is started mid-gesture that wasn't a part of a
             // previous quickswitch task launch, then cancel the animation back to the app
             RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
             TaskInfo taskInfo = appearedTaskTarget.taskInfo;
-            ActiveGestureLog.INSTANCE.addLog(forceFinishReason
-                            .append("Unexpected task appeared id=")
-                            .append(taskInfo.taskId)
-                            .append(" pkg=")
-                            .append(taskInfo.baseIntent.getComponent().getPackageName()));
+            ActiveGestureProtoLogProxy.logUnexpectedTaskAppeared(
+                    taskInfo.taskId,
+                    taskInfo.baseIntent.getComponent().getPackageName());
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         ActiveGestureLog.CompoundString handleTaskFailureReason =
-                new ActiveGestureLog.CompoundString("handleTaskAppeared check failed: ");
+                ActiveGestureLog.CompoundString.newEmptyString();
         if (!handleTaskAppeared(appearedTaskTargets, handleTaskFailureReason)) {
-            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append(handleTaskFailureReason));
+            forceFinishReason.append(handleTaskFailureReason);
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
@@ -2456,8 +2404,8 @@
                 .filter(mGestureState.mLastStartedTaskIdPredicate)
                 .toArray(RemoteAnimationTarget[]::new);
         if (taskTargets.length == 0) {
-            ActiveGestureLog.INSTANCE.addLog(
-                    forceFinishReason.append("No appeared task matching started task id"));
+            forceFinishReason.append("No appeared task matching started task id");
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
@@ -2466,12 +2414,14 @@
                 ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
         if (taskView == null || taskView.getTaskContainers().stream().noneMatch(
                 TaskContainer::getShouldShowSplashView)) {
-            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Splash not needed"));
+            forceFinishReason.append("Splash not needed");
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         if (mContainer == null) {
-            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Activity destroyed"));
+            forceFinishReason.append("Activity destroyed");
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
@@ -2479,7 +2429,7 @@
     }
 
     private void animateSplashScreenExit(
-            @NonNull T activity,
+            @NonNull RECENTS_CONTAINER activity,
             @NonNull RemoteAnimationTarget[] appearedTaskTargets,
             @NonNull RemoteAnimationTarget[] animatingTargets) {
         ViewGroup splashView = activity.getDragLayer();
@@ -2527,7 +2477,7 @@
         if (mRecentsAnimationController != null) {
             mRecentsAnimationController.finish(false /* toRecents */, onFinishComplete);
         }
-        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
+        ActiveGestureProtoLogProxy.logFinishRecentsAnimationOnTasksAppeared();
     }
 
     /**
@@ -2562,7 +2512,7 @@
         // Preload the plan
         RecentsModel.INSTANCE.get(mContext).getTasks(null);
 
-        mActivityInitListener.register(reasonString);
+        mContextInitListener.register(reasonString);
     }
 
     private boolean shouldFadeOutTargetsForKeyboardQuickSwitch(
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 00cd60b..1e755eb 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -21,21 +21,21 @@
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_ATTACHED_ALPHA_ANIM;
 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.RUNNING_TASK_ATTACH_ALPHA;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.graphics.Color;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
 
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
@@ -61,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;
     }
 
     /**
@@ -123,17 +120,10 @@
         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 =
-                controller != null && controller.isEventOverBubbleBarStashHandle(ev);
+                controller != null && controller.isEventOverBubbleBarViews(ev);
         return deviceState.isInDeferredGestureRegion(ev) || deviceState.isImeRenderingNavButtons()
                 || isTrackpadMultiFingerSwipe(ev) || isEventOverBubbleBarStashHandle;
     }
@@ -162,47 +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.
-         */
-        default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
-
-        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;
@@ -231,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();
@@ -254,12 +203,14 @@
             // (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(mActivity) == NavigationMode.NO_BUTTON) {
-                setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
+                setRecentsAttachedToAppWindow(
+                        mIsAttachedToWindow, false, recentsView.shouldUpdateRunningTaskAlpha());
             }
         }
 
         @Override
-        public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
+        public void setRecentsAttachedToAppWindow(
+                boolean attached, boolean animate, boolean updateRunningTaskAlpha) {
             if (mIsAttachedToWindow == attached && animate) {
                 return;
             }
@@ -267,6 +218,10 @@
                     .cancelStateElementAnimation(INDEX_RECENTS_FADE_ANIM);
             mActivity.getStateManager()
                     .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+            if (updateRunningTaskAlpha) {
+                mActivity.getStateManager()
+                        .cancelStateElementAnimation(INDEX_RECENTS_ATTACHED_ALPHA_ANIM);
+            }
 
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.addListener(new AnimatorListenerAdapter() {
@@ -281,19 +236,28 @@
 
             long animationDuration = animate ? RECENTS_ATTACH_DURATION : 0;
             Animator fadeAnim = mActivity.getStateManager()
-                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1f : 0f);
             fadeAnim.setInterpolator(attached ? INSTANT : ACCELERATE_2);
             fadeAnim.setDuration(animationDuration);
             animatorSet.play(fadeAnim);
 
             float fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(
                     mActivity.getOverviewPanel());
-            float toTranslation = attached ? 0 : 1;
-
+            float toTranslation = attached ? 0f : 1f;
             Animator translationAnimator = mActivity.getStateManager().createStateElementAnimation(
                     INDEX_RECENTS_TRANSLATE_X_ANIM, fromTranslation, toTranslation);
             translationAnimator.setDuration(animationDuration);
             animatorSet.play(translationAnimator);
+
+            if (updateRunningTaskAlpha) {
+                float fromAlpha = RUNNING_TASK_ATTACH_ALPHA.get(mActivity.getOverviewPanel());
+                float toAlpha = attached ? 1f : 0f;
+                Animator runningTaskAttachAlphaAnimator = mActivity.getStateManager()
+                        .createStateElementAnimation(
+                                INDEX_RECENTS_ATTACHED_ALPHA_ANIM, fromAlpha, toAlpha);
+                runningTaskAttachAlphaAnimator.setDuration(animationDuration);
+                animatorSet.play(runningTaskAttachAlphaAnimator);
+            }
             animatorSet.start();
         }
 
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index 3a8c141..a3953ca 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,28 +59,38 @@
 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);
 
-    public abstract boolean allowMinimizeSplitScreen();
-
     public abstract boolean isResumed();
 
     public abstract boolean isStarted();
     public abstract boolean deferStartingActivity(RecentsAnimationDeviceState deviceState,
             MotionEvent ev);
 
-    /** @return whether to allow going to All Apps from Overview. */
-    public abstract boolean allowAllAppsFromOverview();
-
     /**
      * Returns the color of the scrim behind overview when at rest in this state.
      * Return {@link Color#TRANSPARENT} for no scrim.
@@ -93,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.
@@ -131,9 +168,22 @@
         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() {
-        return null;
+        CONTAINER_TYPE container = getCreatedContainer();
+
+        return container == null ? null : container.getDesktopVisibilityController();
     }
 
     /**
@@ -328,9 +378,6 @@
     public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) {
         out.x = dp.widthPx;
         out.y = dp.heightPx;
-        if (dp.isTablet && !DisplayController.isTransientTaskbar(context)) {
-            out.y -= dp.taskbarHeight;
-        }
     }
 
     /**
@@ -406,4 +453,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/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index 358f644..94f4920 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -24,8 +24,8 @@
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskContainer
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 
 /** A menu item, "Desktop", that allows the user to bring the current app into Desktop Windowing. */
 class DesktopSystemShortcut(
@@ -34,7 +34,7 @@
     abstractFloatingViewHelper: AbstractFloatingViewHelper
 ) :
     SystemShortcut<RecentsViewContainer>(
-        R.drawable.ic_caption_desktop_button_foreground,
+        R.drawable.ic_desktop,
         R.string.recent_task_option_desktop,
         container,
         taskContainer.itemInfo,
diff --git a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
index 3549a12..f610014 100644
--- a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
+++ b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
@@ -52,7 +52,11 @@
         )
 
     val lpnhTimeoutMs =
-        propReader.get("LPNH_TIMEOUT_MS", 450, "Controls lpnh timeout in milliseconds")
+        propReader.get(
+            "LPNH_TIMEOUT_MS",
+            DEFAULT_LPNH_TIMEOUT_MS,
+            "Controls lpnh timeout in milliseconds"
+        )
 
     val lpnhSlopPercentage =
         propReader.get("LPNH_SLOP_PERCENTAGE", 100, "Controls touch slop percentage for lpnh")
@@ -64,11 +68,19 @@
             "Enable two stage for LPNH duration and touch slop"
         )
 
-    val twoStageMultiplier =
+    val twoStageDurationPercentage =
         propReader.get(
-            "TWO_STAGE_MULTIPLIER",
-            2,
-            "Extends the duration and touch slop if the initial slop is passed"
+            "TWO_STAGE_DURATION_PERCENTAGE",
+            200,
+            "Extends the duration to trigger a long press after a fraction of the gesture " +
+                "slop is passed, expressed as a percentage (i.e. 200 = 2x)."
+        )
+
+    val twoStageSlopPercentage =
+        propReader.get(
+            "TWO_STAGE_SLOP_PERCENTAGE",
+            50,
+            "Percentage of gesture slop region to trigger the extended long press duration."
         )
 
     val animateLpnh = propReader.get("ANIMATE_LPNH", false, "Animates navbar when long pressing")
@@ -130,13 +142,6 @@
             "Controls extra dp on the nav bar sides to trigger LPNH. Can be negative for a smaller touch region."
         )
 
-    val allAppsOverviewThreshold =
-        propReader.get(
-            "ALL_APPS_OVERVIEW_THRESHOLD",
-            180,
-            "Threshold to open All Apps from Overview"
-        )
-
     /** Dump config values. */
     fun dump(prefix: String, writer: PrintWriter) {
         writer.println("$prefix DeviceConfigWrapper:")
@@ -157,12 +162,13 @@
         writer.println("$prefix\tenableLpnhDeepPress=$enableLpnhDeepPress")
         writer.println("$prefix\tlpnhHapticHintDelay=$lpnhHapticHintDelay")
         writer.println("$prefix\tlpnhExtraTouchWidthDp=$lpnhExtraTouchWidthDp")
-        writer.println("$prefix\tallAppsOverviewThreshold=$allAppsOverviewThreshold")
     }
 
     companion object {
         @JvmStatic val configHelper by lazy { DeviceConfigHelper(::DeviceConfigWrapper) }
 
         @JvmStatic fun get() = configHelper.config
+
+        const val DEFAULT_LPNH_TIMEOUT_MS = 450
     }
 }
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 89fbf4a..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
@@ -133,17 +133,6 @@
     }
 
     @Override
-    public boolean allowMinimizeSplitScreen() {
-        // TODO: Remove this once b/77875376 is fixed
-        return false;
-    }
-
-    @Override
-    public boolean allowAllAppsFromOverview() {
-        return false;
-    }
-
-    @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
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index 9b66154..7abcfb8 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -64,6 +64,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
@@ -82,7 +83,7 @@
  * Handles the navigation gestures when a 3rd party launcher is the default home activity.
  */
 public class FallbackSwipeHandler extends
-        AbsSwipeUpHandler<RecentsActivity, FallbackRecentsView, RecentsState> {
+        AbsSwipeUpHandler<RecentsActivity, FallbackRecentsView<RecentsActivity>, RecentsState> {
 
     private static final String TAG = "FallbackSwipeHandler";
 
@@ -105,7 +106,7 @@
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
             boolean continuingLastGesture, InputConsumerController inputConsumer) {
         super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
-                continuingLastGesture, inputConsumer);
+                continuingLastGesture, inputConsumer, null);
 
         mRunningOverHome = mGestureState.getRunningTask() != null
                 && mGestureState.getRunningTask().isHomeTask();
diff --git a/quickstep/src/com/android/quickstep/FallbackWindowInterface.java b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
new file mode 100644
index 0000000..832c093
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
+import static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
+import static com.android.quickstep.fallback.RecentsState.DEFAULT;
+import static com.android.quickstep.fallback.RecentsState.HOME;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.RemoteAnimationTarget;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.FallbackTaskbarUIController;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.ContextInitListener;
+import com.android.quickstep.views.RecentsView;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * {@link BaseWindowInterface} for recents when the default launcher is different than the
+ * currently running one and apps should interact with the {@link RecentsWindowManager} as opposed
+ * to the in-launcher one.
+ */
+public final class FallbackWindowInterface extends BaseWindowInterface{
+
+    private static FallbackWindowInterface INSTANCE;
+
+    private final RecentsWindowManager mRecentsWindowManager;
+
+    /**
+     * This is only null before init() or after destroy()
+     */
+    @Nullable
+    public static FallbackWindowInterface getInstance(){
+        return INSTANCE;
+    }
+
+    public static void init(RecentsWindowManager recentsWindowManager) {
+        if (INSTANCE == null) {
+            INSTANCE = new FallbackWindowInterface(recentsWindowManager);
+        }
+    }
+
+    private FallbackWindowInterface(RecentsWindowManager recentsWindowManager) {
+        super(DEFAULT, BACKGROUND_APP);
+        mRecentsWindowManager = recentsWindowManager;
+    }
+
+    public void destroy() {
+        INSTANCE = null;
+    }
+
+    /** 2 */
+    @Override
+    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
+            RecentsPagedOrientationHandler orientationHandler) {
+        calculateTaskSize(context, dp, outRect, orientationHandler);
+        if (dp.isVerticalBarLayout() && DisplayController.getNavigationMode(context) != NO_BUTTON) {
+            return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
+        } else {
+            return dp.heightPx - outRect.bottom;
+        }
+    }
+
+    /** 5 */
+    @Override
+    public void onAssistantVisibilityChanged(float visibility) {
+        // This class becomes active when the screen is locked.
+        // Rather than having it handle assistant visibility changes, the assistant visibility is
+        // set to zero prior to this class becoming active.
+    }
+
+    /** 6 */
+    @Override
+    public BaseWindowInterface.AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState
+            deviceState, boolean activityVisible,
+            Consumer<AnimatorControllerWithResistance> callback) {
+        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+        BaseWindowInterface.DefaultAnimationFactory factory =
+                new BaseWindowInterface.DefaultAnimationFactory(callback);
+        factory.initBackgroundStateUI();
+        return factory;
+    }
+
+    @Override
+    public ContextInitListener<RecentsWindowManager> createActivityInitListener(
+            Predicate<Boolean> onInitListener) {
+        return new ContextInitListener<>(
+                (activity, alreadyOnHome) -> onInitListener.test(alreadyOnHome),
+                RecentsWindowManager.getRecentsWindowTracker());
+    }
+
+    @Nullable
+    @Override
+    public RecentsWindowManager getCreatedContainer() {
+        return mRecentsWindowManager;
+    }
+
+    @Override
+    public FallbackTaskbarUIController getTaskbarController() {
+        RecentsWindowManager manager = getCreatedContainer();
+        if (manager == null) {
+            return null;
+        }
+        return null;
+        // todo b/365775636: pass a taskbar implementation
+        // return manager.getTaskbarUIController();
+    }
+
+    @Override
+    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTarget target) {
+        // TODO: Remove this once b/77875376 is fixed
+        return target.screenSpaceBounds;
+    }
+
+    @Nullable
+    @Override
+    public <T extends RecentsView<?, ?>> T getVisibleRecentsView() {
+        RecentsWindowManager manager = getCreatedContainer();
+        if(manager.isStarted() || isInLiveTileMode()){
+            return getCreatedContainer().getOverviewPanel();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
+        return false;
+    }
+
+    @Override
+    protected int getOverviewScrimColorForState(RecentsWindowManager container,
+            RecentsState state) {
+        return state.getScrimColor(container.asContext());
+    }
+
+    @Override
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        // In non-gesture mode, user might be clicking on the home button which would directly
+        // start the home activity instead of going through recents. In that case, defer starting
+        // recents until we are sure it is a gesture.
+        return false;
+//        return !deviceState.isFullyGesturalNavMode();
+//                || super.deferStartingActivity(deviceState, ev);
+    }
+
+    @Override
+    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
+        final StateManager<RecentsState, RecentsWindowManager> stateManager =
+                getCreatedContainer().getStateManager();
+        if (stateManager.getState() == HOME) {
+            exitRunnable.run();
+            notifyRecentsOfOrientation(deviceState);
+            return;
+        }
+
+        stateManager.addStateListener(
+                new StateManager.StateListener<RecentsState>() {
+                    @Override
+                    public void onStateTransitionComplete(RecentsState toState) {
+                        // Are we going from Recents to Workspace?
+                        if (toState == HOME) {
+                            exitRunnable.run();
+                            notifyRecentsOfOrientation(deviceState);
+                            stateManager.removeStateListener(this);
+                        }
+                    }
+                });
+    }
+
+    @Override
+    public boolean isInLiveTileMode() {
+        RecentsWindowManager windowManager = getCreatedContainer();
+        return windowManager != null && windowManager.getStateManager().getState() == DEFAULT &&
+                windowManager.isStarted();
+    }
+
+    @Override
+    public void onLaunchTaskFailed() {
+        // TODO: probably go back to overview instead.
+        RecentsWindowManager manager = getCreatedContainer();
+        if (manager == null) {
+            return;
+        }
+        manager.<RecentsView>getOverviewPanel().startHome();
+    }
+
+    @Override
+    public RecentsState stateFromGestureEndTarget(GestureEndTarget endTarget) {
+        switch (endTarget) {
+            case RECENTS:
+                return DEFAULT;
+            case NEW_TASK:
+            case LAST_TASK:
+                return BACKGROUND_APP;
+            case HOME:
+            case ALL_APPS:
+            default:
+                return HOME;
+        }
+    }
+
+    private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
+        // reset layout on swipe to home
+        RecentsView recentsView = getCreatedContainer().getOverviewPanel();
+        recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+                rotationTouchHelper.getDisplayRotation());
+    }
+
+    @Override
+    public @Nullable Animator getParallelAnimationToLauncher(GestureEndTarget endTarget,
+            long duration, RecentsAnimationCallbacks callbacks) {
+        FallbackTaskbarUIController uiController = getTaskbarController();
+        Animator superAnimator = super.getParallelAnimationToLauncher(
+                endTarget, duration, callbacks);
+        if (uiController == null) {
+            return superAnimator;
+        }
+        RecentsState toState = stateFromGestureEndTarget(endTarget);
+        Animator taskbarAnimator = uiController.createAnimToRecentsState(toState, duration);
+        if (taskbarAnimator == null) {
+            return superAnimator;
+        }
+        if (superAnimator == null) {
+            return taskbarAnimator;
+        }
+        AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.playTogether(superAnimator, taskbarAnimator);
+        return animatorSet;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/FocusState.kt b/quickstep/src/com/android/quickstep/FocusState.kt
new file mode 100644
index 0000000..ba3991f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FocusState.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.os.RemoteException
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.launcher3.util.Executors
+import com.android.wm.shell.shared.IFocusTransitionListener.Stub
+import com.android.wm.shell.shared.IShellTransitions
+
+/** Class to track focus state of displays and windows */
+class FocusState {
+
+    var focusedDisplayId = DEFAULT_DISPLAY
+        private set
+
+    private var listeners = mutableSetOf<FocusChangeListener>()
+
+    fun addListener(l: FocusChangeListener) = listeners.add(l)
+
+    fun removeListener(l: FocusChangeListener) = listeners.remove(l)
+
+    fun init(transitions: IShellTransitions?) {
+        try {
+            transitions?.setFocusTransitionListener(
+                object : Stub() {
+                    override fun onFocusedDisplayChanged(displayId: Int) {
+                        Executors.MAIN_EXECUTOR.execute {
+                            listeners.forEach { it.onFocusedDisplayChanged(displayId) }
+                        }
+                    }
+                }
+            )
+        } catch (e: RemoteException) {
+            Log.w(TAG, "Failed call setFocusTransitionListener", e)
+        }
+    }
+
+    interface FocusChangeListener {
+        fun onFocusedDisplayChanged(displayId: Int)
+    }
+
+    override fun toString() = "{FocusState focusedDisplayId=$focusedDisplayId}"
+
+    companion object {
+        private const val TAG = "FocusState"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 81c9d4a..cff352c 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -24,7 +24,6 @@
 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;
@@ -37,10 +36,13 @@
 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;
 
@@ -179,7 +181,6 @@
     private RemoteAnimationTarget[] mLastAppearedTaskTargets;
     private Set<Integer> mPreviouslyAppearedTaskIds = new HashSet<>();
     private int[] mLastStartedTaskId = new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
-    private RecentsAnimationController mRecentsAnimationController;
     private HashMap<Integer, ThumbnailData> mRecentsAnimationCanceledSnapshots;
 
     /** The time when the swipe up gesture is triggered. */
@@ -191,7 +192,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;
@@ -270,8 +271,8 @@
     /**
      * @return the interface to the activity handing the UI updates for this gesture.
      */
-    public <S extends BaseState<S>, T extends RecentsViewContainer>
-            BaseContainerInterface<S, T> getContainerInterface() {
+    public <S extends BaseState<S>, T extends RecentsViewContainer & StatefulContainer<S>>
+    BaseContainerInterface getContainerInterface() {
         return mContainerInterface;
     }
 
@@ -302,6 +303,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
@@ -411,10 +424,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);
@@ -470,7 +480,6 @@
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
-        mRecentsAnimationController = controller;
         mStateCallback.setState(STATE_RECENTS_ANIMATION_STARTED);
     }
 
@@ -480,10 +489,6 @@
         mStateCallback.setState(STATE_RECENTS_ANIMATION_CANCELED);
         mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
         if (mRecentsAnimationCanceledSnapshots != null) {
-            // Clean up the screenshot to finalize the recents animation cancel
-            if (mRecentsAnimationController != null) {
-                mRecentsAnimationController.cleanupScreenshot();
-            }
             mRecentsAnimationCanceledSnapshots = null;
         }
     }
@@ -522,7 +527,7 @@
     HashMap<Integer, ThumbnailData> consumeRecentsAnimationCanceledSnapshot() {
         if (mRecentsAnimationCanceledSnapshots != null) {
             HashMap<Integer, ThumbnailData> data =
-                    new HashMap<Integer, ThumbnailData>(mRecentsAnimationCanceledSnapshots);
+                    new HashMap<>(mRecentsAnimationCanceledSnapshots);
             mRecentsAnimationCanceledSnapshots = null;
             return data;
         }
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index f898e2f..0185737 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -40,6 +40,7 @@
     int TYPE_STATUS_BAR = 1 << 13;
     int TYPE_CURSOR_HOVER = 1 << 14;
     int TYPE_NAV_HANDLE_LONG_PRESS = 1 << 15;
+    int TYPE_BUBBLE_BAR = 1 << 16;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -58,6 +59,7 @@
             "TYPE_STATUS_BAR",              // 13
             "TYPE_CURSOR_HOVER",            // 14
             "TYPE_NAV_HANDLE_LONG_PRESS",   // 15
+            "TYPE_BUBBLE_BAR",              // 16
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index b564fa7..ef6a09d 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -18,7 +18,6 @@
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.FLOATING_SEARCH_BAR;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -40,9 +39,7 @@
 import com.android.launcher3.LauncherInitListener;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -50,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;
@@ -79,7 +75,7 @@
                 && DisplayController.getNavigationMode(context) != NavigationMode.NO_BUTTON) {
             return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
         } else {
-            return LayoutUtils.getShelfTrackingDistance(context, dp, orientationHandler);
+            return LayoutUtils.getShelfTrackingDistance(context, dp, orientationHandler, this);
         }
     }
 
@@ -137,7 +133,7 @@
     }
 
     @Override
-    public ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
+    public LauncherInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
         return new LauncherInitListener((activity, alreadyOnHome) ->
                 onInitListener.test(alreadyOnHome));
     }
@@ -154,7 +150,7 @@
     @Nullable
     @Override
     public QuickstepLauncher getCreatedContainer() {
-        return QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+        return QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext();
     }
 
     @Nullable
@@ -169,16 +165,6 @@
 
     @Nullable
     @Override
-    public DesktopVisibilityController getDesktopVisibilityController() {
-        QuickstepLauncher launcher = getCreatedContainer();
-        if (launcher == null) {
-            return null;
-        }
-        return launcher.getDesktopVisibilityController();
-    }
-
-    @Nullable
-    @Override
     public LauncherTaskbarUIController getTaskbarController() {
         QuickstepLauncher launcher = getCreatedContainer();
         if (launcher == null) {
@@ -271,18 +257,6 @@
     }
 
     @Override
-    public boolean allowMinimizeSplitScreen() {
-        return true;
-    }
-
-    @Override
-    public boolean allowAllAppsFromOverview() {
-        return FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()
-                // If floating search bar would not show in overview, don't allow all apps gesture.
-                && OVERVIEW.areElementsVisible(getCreatedContainer(), FLOATING_SEARCH_BAR);
-    }
-
-    @Override
     public boolean isInLiveTileMode() {
         QuickstepLauncher launcher = getCreatedContainer();
 
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index b720382..1124aac 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -27,7 +27,6 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.content.ComponentCallbacks;
 import android.content.res.Configuration;
@@ -38,7 +37,6 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
-import android.util.Pair;
 import android.view.Choreographer;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
@@ -63,7 +61,7 @@
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.BackAnimState;
 import com.android.systemui.shared.system.QuickStepContract;
 
 import java.lang.ref.WeakReference;
@@ -109,8 +107,6 @@
     private RemoteAnimationTarget mLauncherTarget;
     private View mLauncherTargetView;
     private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-    private boolean mSpringAnimationInProgress = false;
-    private boolean mAnimatorSetInProgress = false;
     private float mBackProgress = 0;
     private boolean mBackInProgress = false;
     private OnBackInvokedCallbackStub mBackCallback;
@@ -341,7 +337,9 @@
         mTransaction
                 .setColor(mScrimLayer, colorComponents)
                 .setAlpha(mScrimLayer, mScrimAlpha)
-                .show(mScrimLayer);
+                .show(mScrimLayer)
+                // Ensure the scrim layer occludes opening task & wallpaper
+                .setLayer(mScrimLayer, 1000);
     }
 
     void removeScrimLayer() {
@@ -446,15 +444,15 @@
         mQuickstepTransitionManager.transferRectToTargetCoordinate(
                 mBackTarget, mCurrentRect, true, resolveRectF);
 
-        Pair<RectFSpringAnim, AnimatorSet> pair =
+        BackAnimState backAnim =
                 mQuickstepTransitionManager.createWallpaperOpenAnimations(
                     new RemoteAnimationTarget[]{mBackTarget},
                     new RemoteAnimationTarget[0],
-                    false /* fromUnlock */,
+                    new RemoteAnimationTarget[0],
                     resolveRectF,
                     cornerRadius,
                     mBackInProgress /* fromPredictiveBack */);
-        startTransitionAnimations(pair.first, pair.second);
+        startTransitionAnimations(backAnim);
         mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
         customizeStatusBarAppearance(true);
     }
@@ -469,8 +467,6 @@
         mCurrentRect.setEmpty();
         mStartRect.setEmpty();
         mInitialTouchPos.set(0, 0);
-        mAnimatorSetInProgress = false;
-        mSpringAnimationInProgress = false;
         setLauncherTargetViewVisible(true);
         mLauncherTargetView = null;
         // We don't call customizeStatusBarAppearance here to prevent the status bar update with
@@ -493,27 +489,8 @@
         }
     }
 
-    private void startTransitionAnimations(RectFSpringAnim springAnim, AnimatorSet anim) {
-        mAnimatorSetInProgress = anim != null;
-        mSpringAnimationInProgress = springAnim != null;
-        if (springAnim != null) {
-            springAnim.addAnimatorListener(
-                    new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            mSpringAnimationInProgress = false;
-                            tryFinishBackAnimation();
-                        }
-                    }
-            );
-        }
-        anim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimatorSetInProgress = false;
-                tryFinishBackAnimation();
-            }
-        });
+    private void startTransitionAnimations(BackAnimState backAnim) {
+        backAnim.addOnAnimCompleteCallback(this::finishAnimation);
         if (mScrimLayer == null) {
             // Scrim hasn't been attached yet. Let's attach it.
             addScrimLayer();
@@ -533,7 +510,7 @@
             }
         });
         mScrimAlphaAnimator.setDuration(SCRIM_FADE_DURATION).start();
-        anim.start();
+        backAnim.start();
     }
 
     private void loadResources() {
@@ -566,12 +543,6 @@
         mScrimAlpha = 0;
     }
 
-    private void tryFinishBackAnimation() {
-        if (!mSpringAnimationInProgress && !mAnimatorSetInProgress) {
-            finishAnimation();
-        }
-    }
-
     private void customizeStatusBarAppearance(boolean overridingStatusBarFlags) {
         if (mOverridingStatusBarFlags == overridingStatusBarFlags) {
             return;
diff --git a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
index 27bd03d..7d22c52 100644
--- a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
+++ b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
@@ -5,6 +5,7 @@
 import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType
 import android.app.backup.BackupRestoreEventLogger.BackupRestoreError
 import android.content.Context
+import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags.enableLauncherBrMetricsFixed
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
@@ -29,8 +30,8 @@
         @BackupRestoreDataType private const val DATA_TYPE_APP_PAIR = "app_pair"
     }
 
-    private val restoreEventLogger: BackupRestoreEventLogger =
-        BackupManager(context).delayedRestoreLogger
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    val restoreEventLogger: BackupRestoreEventLogger = BackupManager(context).delayedRestoreLogger
 
     /**
      * For logging when multiple items of a given data type failed to restore.
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index e17cdcd..dacafd4 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.Utilities.mapBoundToRange;
-import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
 
@@ -42,11 +41,12 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.ObjectWrapper;
+import com.android.launcher3.util.StableViewInfo;
 import com.android.launcher3.views.ClipIconView;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.views.FloatingView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
@@ -62,14 +62,14 @@
 /**
  * Temporary class to allow easier refactoring
  */
-public class LauncherSwipeHandlerV2 extends
-        AbsSwipeUpHandler<QuickstepLauncher, RecentsView, LauncherState> {
+public class LauncherSwipeHandlerV2 extends AbsSwipeUpHandler<
+        QuickstepLauncher, RecentsView<QuickstepLauncher, LauncherState>, LauncherState> {
 
     public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
             boolean continuingLastGesture, InputConsumerController inputConsumer) {
         super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
-                continuingLastGesture, inputConsumer);
+                continuingLastGesture, inputConsumer, null);
     }
 
 
@@ -107,9 +107,6 @@
                 || !mContainer.getDesktopVisibilityController().areDesktopTasksVisible());
 
         mContainer.getRootView().setForceHideBackArrow(true);
-        if (!TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            mContainer.setHintUserWillBeActive();
-        }
 
         if (!canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
             return new LauncherHomeAnimationFactory() {
@@ -304,18 +301,7 @@
             return null;
         }
 
-        // Find the associated item info for the launch cookie (if available), note that predicted
-        // apps actually have an id of -1, so use another default id here
-        int launchCookieItemId = NO_MATCHING_ID;
-        for (IBinder cookie : launchCookies) {
-            Integer itemId = ObjectWrapper.unwrap(cookie);
-            if (itemId != null) {
-                launchCookieItemId = itemId;
-                break;
-            }
-        }
-
-        return mContainer.getFirstMatchForAppClose(launchCookieItemId,
+        return mContainer.getFirstMatchForAppClose(StableViewInfo.fromLaunchCookies(launchCookies),
                 sourceTaskView.getFirstTask().key.getComponent().getPackageName(),
                 UserHandle.of(sourceTaskView.getFirstTask().key.userId),
                 false /* supportsAllAppsState */);
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/OrientationRectF.java b/quickstep/src/com/android/quickstep/OrientationRectF.java
index aa01b05..2b7ecb2 100644
--- a/quickstep/src/com/android/quickstep/OrientationRectF.java
+++ b/quickstep/src/com/android/quickstep/OrientationRectF.java
@@ -67,13 +67,15 @@
     }
 
     public boolean applyTransform(MotionEvent event, int deltaRotation, boolean forceTransform) {
+        if (deltaRotation == 0) {
+            return contains(event.getX(), event.getY());
+        }
         mTmpMatrix.reset();
         postDisplayRotation(deltaRotation, mHeight, mWidth, mTmpMatrix);
         if (forceTransform) {
             if (DEBUG) {
                 Log.d(TAG, "Transforming rotation due to forceTransform, "
                         + "deltaRotation: " + deltaRotation
-                        + "mRotation: " + mRotation
                         + " this: " + this);
             }
             event.applyTransform(mTmpMatrix);
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 84f6b55..a03c0f8 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.systemui.shared.Flags;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -242,7 +243,8 @@
         int rotation = display.rotation;
         int touchHeight = mNavBarGesturalHeight;
         OrientationRectF orientationRectF = new OrientationRectF(0, 0, size.x, size.y, rotation);
-        if (mMode == NavigationMode.NO_BUTTON) {
+        if (mMode == NavigationMode.NO_BUTTON
+                || (mMode == NavigationMode.THREE_BUTTONS && Flags.threeButtonCornerSwipe())) {
             orientationRectF.top = orientationRectF.bottom - touchHeight;
             updateAssistantRegions(orientationRectF);
         } else {
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
deleted file mode 100644
index 8f533a3..0000000
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ /dev/null
@@ -1,529 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.PagedView.INVALID_PAGE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.BinderThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.internal.jank.Cuj;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.taskbar.TaskbarUIController;
-import com.android.launcher3.util.RunnableList;
-import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
-import com.android.quickstep.util.ActiveGestureLog;
-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;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-/**
- * Helper class to handle various atomic commands for switching between Overview.
- */
-public class OverviewCommandHelper {
-    private static final String TAG = "OverviewCommandHelper";
-
-    public static final int TYPE_SHOW = 1;
-    public static final int TYPE_KEYBOARD_INPUT = 2;
-    public static final int TYPE_HIDE = 3;
-    public static final int TYPE_TOGGLE = 4;
-    public static final int TYPE_HOME = 5;
-
-    /**
-     * Use case for needing a queue is double tapping recents button in 3 button nav.
-     * Size of 2 should be enough. We'll toss in one more because we're kind hearted.
-     */
-    private final static int MAX_QUEUE_SIZE = 3;
-
-    private static final String TRANSITION_NAME = "Transition:toOverview";
-
-    private final TouchInteractionService mService;
-    private final OverviewComponentObserver mOverviewComponentObserver;
-    private final TaskAnimationManager mTaskAnimationManager;
-    private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>();
-
-    /**
-     * Index of the TaskView that should be focused when launching Overview. Persisted so that we
-     * do not lose the focus across multiple calls of
-     * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command
-     */
-    private int mKeyboardTaskFocusIndex = -1;
-
-    /**
-     * Whether we should incoming toggle commands while a previous toggle command is still ongoing.
-     * This serves as a rate-limiter to prevent overlapping animations that can clobber each other
-     * and prevent clean-up callbacks from running. This thus prevents a recurring set of bugs with
-     * janky recents animations and unresponsive home and overview buttons.
-     */
-    private boolean mWaitForToggleCommandComplete = false;
-
-    public OverviewCommandHelper(TouchInteractionService service,
-            OverviewComponentObserver observer,
-            TaskAnimationManager taskAnimationManager) {
-        mService = service;
-        mOverviewComponentObserver = observer;
-        mTaskAnimationManager = taskAnimationManager;
-    }
-
-    /**
-     * Called when the command finishes execution.
-     */
-    private void scheduleNextTask(CommandInfo command) {
-        if (mPendingCommands.isEmpty()) {
-            Log.d(TAG, "no pending commands to schedule");
-            return;
-        }
-        if (mPendingCommands.get(0) != command) {
-            Log.d(TAG, "next task not scheduled."
-                    + " mPendingCommands[0] type is " + mPendingCommands.get(0)
-                    + " - command type is: " + command);
-            return;
-        }
-        Log.d(TAG, "scheduleNextTask called: " + command);
-        mPendingCommands.remove(0);
-        executeNext();
-    }
-
-    /**
-     * Executes the next command from the queue. If the command finishes immediately (returns true),
-     * it continues to execute the next command, until the queue is empty of a command defer's its
-     * completion (returns false).
-     */
-    @UiThread
-    private void executeNext() {
-        if (mPendingCommands.isEmpty()) {
-            Log.d(TAG, "executeNext - mPendingCommands is empty");
-            return;
-        }
-        CommandInfo cmd = mPendingCommands.get(0);
-
-        boolean result = executeCommand(cmd);
-        Log.d(TAG, "executeNext cmd type: " + cmd + ", result: " + result);
-        if (result) {
-            scheduleNextTask(cmd);
-        }
-    }
-
-    @UiThread
-    private void addCommand(CommandInfo cmd) {
-        boolean wasEmpty = mPendingCommands.isEmpty();
-        mPendingCommands.add(cmd);
-        if (wasEmpty) {
-            executeNext();
-        }
-    }
-
-    /**
-     * Adds a command to be executed next, after all pending tasks are completed.
-     * Max commands that can be queued is {@link #MAX_QUEUE_SIZE}.
-     * Requests after reaching that limit will be silently dropped.
-     */
-    @BinderThread
-    public void addCommand(int type) {
-        if (mPendingCommands.size() >= MAX_QUEUE_SIZE) {
-            Log.d(TAG, "the pending command queue is full (" + mPendingCommands.size() + "). "
-                    + "command not added: " + type);
-            return;
-        }
-        Log.d(TAG, "adding command type: " + type);
-        CommandInfo cmd = new CommandInfo(type);
-        MAIN_EXECUTOR.execute(() -> addCommand(cmd));
-    }
-
-    @UiThread
-    public void clearPendingCommands() {
-        Log.d(TAG, "clearing pending commands - size: " + mPendingCommands.size());
-        mPendingCommands.clear();
-    }
-
-    @UiThread
-    public boolean canStartHomeSafely() {
-        return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME;
-    }
-
-    @Nullable
-    private TaskView getNextTask(RecentsView view) {
-        final TaskView runningTaskView = view.getRunningTaskView();
-
-        if (runningTaskView == null) {
-            return view.getTaskViewAt(0);
-        } else {
-            final TaskView nextTask = view.getNextTaskView();
-            return nextTask != null ? nextTask : runningTaskView;
-        }
-    }
-
-    private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) {
-        RunnableList callbackList = null;
-        if (taskView != null) {
-            mWaitForToggleCommandComplete = true;
-            taskView.setEndQuickSwitchCuj(true);
-            callbackList = taskView.launchTasks();
-        }
-
-        if (callbackList != null) {
-            callbackList.add(() -> {
-                Log.d(TAG, "launching task callback: " + cmd);
-                scheduleNextTask(cmd);
-                mWaitForToggleCommandComplete = false;
-            });
-            Log.d(TAG, "launching task - waiting for callback: " + cmd);
-            return false;
-        } else {
-            recents.startHome();
-            mWaitForToggleCommandComplete = false;
-            return true;
-        }
-    }
-
-    /**
-     * Executes the task and returns true if next task can be executed. If false, then the next
-     * task is deferred until {@link #scheduleNextTask} is called
-     */
-    private <T extends StatefulActivity<?> & RecentsViewContainer> boolean executeCommand(
-            CommandInfo cmd) {
-        if (mWaitForToggleCommandComplete && cmd.type == TYPE_TOGGLE) {
-            Log.d(TAG, "executeCommand: " + cmd
-                    + " - waiting for toggle command complete");
-            return true;
-        }
-        BaseActivityInterface<?, T> activityInterface =
-                mOverviewComponentObserver.getActivityInterface();
-
-        RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
-        RecentsView<?, ?> createdRecentsView;
-
-        Log.d(TAG, "executeCommand: " + cmd
-                + " - visibleRecentsView: " + visibleRecentsView);
-        if (visibleRecentsView == null) {
-            T activity = activityInterface.getCreatedContainer();
-            createdRecentsView = activity == null ? null : activity.getOverviewPanel();
-            DeviceProfile dp = activity == null ? null : activity.getDeviceProfile();
-            TaskbarUIController uiController = activityInterface.getTaskbarController();
-            boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
-                    && uiController != null
-                    && dp != null
-                    && (dp.isTablet || dp.isTwoPanels);
-
-            switch (cmd.type) {
-                case TYPE_HIDE:
-                    if (!allowQuickSwitch) {
-                        return true;
-                    }
-                    mKeyboardTaskFocusIndex = uiController.launchFocusedTask();
-                    if (mKeyboardTaskFocusIndex == -1) {
-                        return true;
-                    }
-                    break;
-                case TYPE_KEYBOARD_INPUT:
-                    if (allowQuickSwitch) {
-                        uiController.openQuickSwitchView();
-                        return true;
-                    } else {
-                        mKeyboardTaskFocusIndex = 0;
-                        break;
-                    }
-                case TYPE_HOME:
-                    ActiveGestureLog.INSTANCE.addLog(
-                            "OverviewCommandHelper.executeCommand(TYPE_HOME)");
-                    // 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
-                    // could potentially delay resuming launcher. See b/348668521 for more details.
-                    mService.startActivity(mOverviewComponentObserver.getHomeIntent());
-                    return true;
-                case TYPE_SHOW:
-                    // When Recents is not currently visible, the command's type is TYPE_SHOW
-                    // when overview is triggered via the keyboard overview button or Action+Tab
-                    // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button
-                    // nav is TYPE_TOGGLE.
-                    mKeyboardTaskFocusIndex = 0;
-                    break;
-                default:
-                    // continue below to handle displaying Recents.
-            }
-        } else {
-            createdRecentsView = visibleRecentsView;
-            switch (cmd.type) {
-                case TYPE_SHOW:
-                    // already visible
-                    return true;
-                case TYPE_KEYBOARD_INPUT: {
-                    if (visibleRecentsView.isHandlingTouch()) {
-                        return true;
-                    }
-                }
-                case TYPE_HIDE: {
-                    if (visibleRecentsView.isHandlingTouch()) {
-                        return true;
-                    }
-                    mKeyboardTaskFocusIndex = INVALID_PAGE;
-                    int currentPage = visibleRecentsView.getNextPage();
-                    TaskView tv = (currentPage >= 0
-                            && currentPage < visibleRecentsView.getTaskViewCount())
-                            ? (TaskView) visibleRecentsView.getPageAt(currentPage)
-                            : null;
-                    return launchTask(visibleRecentsView, tv, cmd);
-                }
-                case TYPE_TOGGLE:
-                    return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd);
-                case TYPE_HOME:
-                    visibleRecentsView.startHome();
-                    return true;
-            }
-        }
-
-        if (createdRecentsView != null) {
-            createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex);
-        }
-        // Handle recents view focus when launching from home
-        Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
-
-            @Override
-            public void onAnimationStart(Animator animation) {
-                super.onAnimationStart(animation);
-                updateRecentsViewFocus(cmd);
-                logShowOverviewFrom(cmd.type);
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                Log.d(TAG, "switching to Overview state - onAnimationEnd: " + cmd);
-                super.onAnimationEnd(animation);
-                onRecentsViewFocusUpdated(cmd);
-                scheduleNextTask(cmd);
-            }
-        };
-        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
-            Log.d(TAG, "switching to Overview state - waiting: " + cmd);
-            // If successfully switched, wait until animation finishes
-            return false;
-        }
-
-        final T activity = activityInterface.getCreatedContainer();
-        if (activity != null) {
-            InteractionJankMonitorWrapper.begin(
-                    activity.getRootView(),
-                    Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
-        }
-
-        GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE,
-                GestureState.TrackpadGestureType.NONE);
-        gestureState.setHandlingAtomicEvent(true);
-        AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory()
-                .newHandler(gestureState, cmd.createTime);
-        interactionHandler.setGestureEndCallback(
-                () -> onTransitionComplete(cmd, interactionHandler));
-        interactionHandler.initWhenReady("OverviewCommandHelper: cmd.type=" + cmd.type);
-
-        RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() {
-            @Override
-            public void onRecentsAnimationStart(RecentsAnimationController controller,
-                    RecentsAnimationTargets targets) {
-                updateRecentsViewFocus(cmd);
-                logShowOverviewFrom(cmd.type);
-                activityInterface.runOnInitBackgroundStateUI(() ->
-                        interactionHandler.onGestureEnded(0, new PointF()));
-                cmd.removeListener(this);
-            }
-
-            @Override
-            public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
-                interactionHandler.onGestureCancelled();
-                cmd.removeListener(this);
-
-                T createdActivity = activityInterface.getCreatedContainer();
-                if (createdActivity == null) {
-                    return;
-                }
-                if (createdRecentsView != null) {
-                    createdRecentsView.onRecentsAnimationComplete();
-                }
-            }
-        };
-
-        if (visibleRecentsView != null) {
-            visibleRecentsView.moveRunningTaskToFront();
-        }
-        if (mTaskAnimationManager.isRecentsAnimationRunning()) {
-            cmd.mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(gestureState);
-            cmd.mActiveCallbacks.addListener(interactionHandler);
-            mTaskAnimationManager.notifyRecentsAnimationState(interactionHandler);
-            interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/);
-
-            cmd.mActiveCallbacks.addListener(recentAnimListener);
-            mTaskAnimationManager.notifyRecentsAnimationState(recentAnimListener);
-        } else {
-            Intent intent = new Intent(interactionHandler.getLaunchIntent());
-            intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, gestureState.getGestureId());
-            cmd.mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(
-                    gestureState, intent, interactionHandler);
-            interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/);
-            cmd.mActiveCallbacks.addListener(recentAnimListener);
-        }
-        Trace.beginAsyncSection(TRANSITION_NAME, 0);
-        Log.d(TAG, "switching via recents animation - onGestureStarted: " + cmd);
-        return false;
-    }
-
-    private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
-        Log.d(TAG, "switching via recents animation - onTransitionComplete: " + cmd);
-        cmd.removeListener(handler);
-        Trace.endAsyncSection(TRANSITION_NAME, 0);
-        onRecentsViewFocusUpdated(cmd);
-        scheduleNextTask(cmd);
-    }
-
-    private void updateRecentsViewFocus(CommandInfo cmd) {
-        RecentsView recentsView =
-                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
-        if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE
-                && cmd.type != TYPE_SHOW)) {
-            return;
-        }
-        // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT),
-        // the touch mode somehow is not change to false by the Android framework.
-        // The subsequent tab to go through tasks in overview can only be dispatched to
-        // focuses views, while focus can only be requested in
-        // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
-        // here we launch overview with live tile.
-        recentsView.getViewRootImpl().touchModeChanged(false);
-        // Ensure that recents view has focus so that it receives the followup key inputs
-        if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) {
-            return;
-        }
-        if (requestFocus(recentsView.getNextTaskView())) {
-            return;
-        }
-        if (requestFocus(recentsView.getTaskViewAt(0))) {
-            return;
-        }
-        requestFocus(recentsView);
-    }
-
-    private void onRecentsViewFocusUpdated(CommandInfo cmd) {
-        RecentsView recentsView =
-                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
-        if (recentsView == null
-                || cmd.type != TYPE_HIDE
-                || mKeyboardTaskFocusIndex == INVALID_PAGE) {
-            return;
-        }
-        recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE);
-        recentsView.setCurrentPage(mKeyboardTaskFocusIndex);
-        mKeyboardTaskFocusIndex = INVALID_PAGE;
-    }
-
-    private boolean requestFocus(@Nullable View taskView) {
-        if (taskView == null) {
-            return false;
-        }
-        taskView.post(() -> {
-            taskView.requestFocus();
-            taskView.requestAccessibilityFocus();
-        });
-        return true;
-    }
-
-    private <T extends StatefulActivity<?> & RecentsViewContainer>
-            void logShowOverviewFrom(int cmdType) {
-        BaseActivityInterface<?, T> activityInterface =
-                mOverviewComponentObserver.getActivityInterface();
-        var container = activityInterface.getCreatedContainer();
-        if (container != null) {
-            StatsLogManager.LauncherEvent event;
-            switch (cmdType) {
-                case TYPE_SHOW -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT;
-                case TYPE_HIDE ->
-                        event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH;
-                case TYPE_TOGGLE -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON;
-                default -> {
-                    return;
-                }
-            }
-
-            StatsLogManager.newInstance(container.asContext())
-                    .logger()
-                    .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
-                            .setTaskSwitcherContainer(
-                                    LauncherAtom.TaskSwitcherContainer.getDefaultInstance())
-                            .build())
-                    .log(event);
-        }
-    }
-
-    public void dump(PrintWriter pw) {
-        pw.println("OverviewCommandHelper:");
-        pw.println("  mPendingCommands=" + mPendingCommands.size());
-        if (!mPendingCommands.isEmpty()) {
-            pw.println("    pendingCommandType=" + mPendingCommands.get(0).type);
-        }
-        pw.println("  mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex);
-        pw.println("  mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete);
-    }
-
-    private static class CommandInfo {
-        public final long createTime = SystemClock.elapsedRealtime();
-        public final int type;
-        RecentsAnimationCallbacks mActiveCallbacks;
-
-        CommandInfo(int type) {
-            this.type = type;
-        }
-
-        void removeListener(RecentsAnimationListener listener) {
-            if (mActiveCallbacks != null) {
-                mActiveCallbacks.removeListener(listener);
-            }
-        }
-
-        @NonNull
-        @Override
-        public String toString() {
-            return "CommandInfo("
-                    + "type=" + type + ", "
-                    + "createTime=" + createTime + ", "
-                    + "mActiveCallbacks=" + mActiveCallbacks
-                    + ")";
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
new file mode 100644
index 0000000..461f963
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.content.Intent
+import android.graphics.PointF
+import android.os.SystemClock
+import android.os.Trace
+import android.util.Log
+import android.view.View
+import androidx.annotation.BinderThread
+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
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.RunnableList
+import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.launcher3.util.coroutines.ProductionDispatchers
+import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus
+import com.android.quickstep.OverviewCommandHelper.CommandType.HIDE
+import com.android.quickstep.OverviewCommandHelper.CommandType.HOME
+import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT
+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.TaskView
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentLinkedDeque
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
+
+/** Helper class to handle various atomic commands for switching between Overview. */
+class OverviewCommandHelper
+@JvmOverloads
+constructor(
+    private val touchInteractionService: TouchInteractionService,
+    private val overviewComponentObserver: OverviewComponentObserver,
+    private val taskAnimationManager: TaskAnimationManager,
+    private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
+) {
+    private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.default)
+
+    private val commandQueue = ConcurrentLinkedDeque<CommandInfo>()
+
+    /**
+     * Index of the TaskView that should be focused when launching Overview. Persisted so that we do
+     * not lose the focus across multiple calls of [OverviewCommandHelper.executeCommand] for the
+     * same command
+     */
+    private var keyboardTaskFocusIndex = -1
+
+    private val containerInterface: BaseContainerInterface<*, *>
+        get() = overviewComponentObserver.containerInterface
+
+    private val visibleRecentsView: RecentsView<*, *>?
+        get() = containerInterface.getVisibleRecentsView<RecentsView<*, *>>()
+
+    /**
+     * Adds a command to be executed next, after all pending tasks are completed. Max commands that
+     * can be queued is [.MAX_QUEUE_SIZE]. Requests after reaching that limit will be silently
+     * dropped.
+     */
+    @BinderThread
+    fun addCommand(type: CommandType): CommandInfo? {
+        if (commandQueue.size >= MAX_QUEUE_SIZE) {
+            Log.d(TAG, "command not added: $type - queue is full ($commandQueue).")
+            return null
+        }
+
+        val command = CommandInfo(type)
+        commandQueue.add(command)
+        Log.d(TAG, "command added: $command")
+
+        if (commandQueue.size == 1) {
+            Log.d(TAG, "execute: $command - queue size: ${commandQueue.size}")
+            if (enableOverviewCommandHelperTimeout()) {
+                coroutineScope.launch(dispatcherProvider.main) { processNextCommand() }
+            } else {
+                Executors.MAIN_EXECUTOR.execute { processNextCommand() }
+            }
+        } else {
+            Log.d(TAG, "not executed: $command - queue size: ${commandQueue.size}")
+        }
+
+        return command
+    }
+
+    fun canStartHomeSafely(): Boolean = commandQueue.isEmpty() || commandQueue.first().type == HOME
+
+    /** Clear pending or completed commands from the queue */
+    fun clearPendingCommands() {
+        Log.d(TAG, "clearing pending commands: $commandQueue")
+        commandQueue.removeAll { it.status != CommandStatus.PROCESSING }
+    }
+
+    /**
+     * Executes the next command from the queue. If the command finishes immediately (returns true),
+     * it continues to execute the next command, until the queue is empty of a command defer's its
+     * completion (returns false).
+     */
+    @UiThread
+    private fun processNextCommand() {
+        val command: CommandInfo =
+            commandQueue.firstOrNull()
+                ?: run {
+                    Log.d(TAG, "no pending commands to be executed.")
+                    return
+                }
+
+        command.status = CommandStatus.PROCESSING
+        Log.d(TAG, "executing command: $command")
+
+        if (enableOverviewCommandHelperTimeout()) {
+            coroutineScope.launch(dispatcherProvider.main) {
+                withTimeout(QUEUE_WAIT_DURATION_IN_MS) {
+                    executeCommandSuspended(command)
+                    ensureActive()
+                    onCommandFinished(command)
+                }
+            }
+        } else {
+            val result = executeCommand(command, onCallbackResult = { onCommandFinished(command) })
+            Log.d(TAG, "command executed: $command with result: $result")
+            if (result) {
+                onCommandFinished(command)
+            } else {
+                Log.d(TAG, "waiting for command callback: $command")
+            }
+        }
+    }
+
+    /**
+     * Executes the task and returns true if next task can be executed. If false, then the next task
+     * is deferred until [.scheduleNextTask] is called
+     */
+    @VisibleForTesting
+    fun executeCommand(command: CommandInfo, onCallbackResult: () -> Unit): Boolean {
+        val recentsView = visibleRecentsView
+        Log.d(TAG, "executeCommand: $command - visibleRecentsView: $recentsView")
+        return if (recentsView != null) {
+            executeWhenRecentsIsVisible(command, recentsView, onCallbackResult)
+        } else {
+            executeWhenRecentsIsNotVisible(command, onCallbackResult)
+        }
+    }
+
+    /**
+     * Executes the task and returns true if next task can be executed. If false, then the next task
+     * is deferred until [.scheduleNextTask] is called
+     */
+    private suspend fun executeCommandSuspended(command: CommandInfo) =
+        suspendCancellableCoroutine { continuation ->
+            fun processResult(isCompleted: Boolean) {
+                Log.d(TAG, "command executed: $command with result: $isCompleted")
+                if (isCompleted) {
+                    continuation.resume(Unit)
+                } else {
+                    Log.d(TAG, "waiting for command callback: $command")
+                }
+            }
+
+            val result = executeCommand(command, onCallbackResult = { processResult(true) })
+            processResult(result)
+
+            continuation.invokeOnCancellation { cancelCommand(command, it) }
+        }
+
+    private fun executeWhenRecentsIsVisible(
+        command: CommandInfo,
+        recentsView: RecentsView<*, *>,
+        onCallbackResult: () -> Unit,
+    ): Boolean =
+        when (command.type) {
+            SHOW -> true // already visible
+            KEYBOARD_INPUT,
+            HIDE -> {
+                if (recentsView.isHandlingTouch) {
+                    true
+                } else {
+                    keyboardTaskFocusIndex = PagedView.INVALID_PAGE
+                    val currentPage = recentsView.nextPage
+                    val taskView = recentsView.getTaskViewAt(currentPage)
+                    launchTask(recentsView, taskView, command, onCallbackResult)
+                }
+            }
+            TOGGLE -> {
+                launchTask(
+                    recentsView,
+                    getNextToggledTaskView(recentsView),
+                    command,
+                    onCallbackResult,
+                )
+            }
+            HOME -> {
+                recentsView.startHome()
+                true
+            }
+        }
+
+    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?,
+        command: CommandInfo,
+        onCallbackResult: () -> Unit,
+    ): Boolean {
+        var callbackList: RunnableList? = null
+        if (taskView != null) {
+            taskView.isEndQuickSwitchCuj = true
+            callbackList = taskView.launchWithAnimation()
+        }
+
+        if (callbackList != null) {
+            callbackList.add {
+                Log.d(TAG, "launching task callback: $command")
+                onCallbackResult()
+            }
+            Log.d(TAG, "launching task - waiting for callback: $command")
+            return false
+        } else {
+            recents.startHome()
+            return true
+        }
+    }
+
+    private fun executeWhenRecentsIsNotVisible(
+        command: CommandInfo,
+        onCallbackResult: () -> Unit,
+    ): Boolean {
+        val recentsViewContainer = containerInterface.getCreatedContainer()
+        val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
+        val deviceProfile = recentsViewContainer?.getDeviceProfile()
+        val uiController = containerInterface.getTaskbarController()
+        val allowQuickSwitch =
+            uiController != null &&
+                deviceProfile != null &&
+                (deviceProfile.isTablet || deviceProfile.isTwoPanels)
+
+        when (command.type) {
+            HIDE -> {
+                if (!allowQuickSwitch) return true
+                keyboardTaskFocusIndex = uiController!!.launchFocusedTask()
+                if (keyboardTaskFocusIndex == -1) return true
+            }
+            KEYBOARD_INPUT ->
+                if (allowQuickSwitch) {
+                    uiController!!.openQuickSwitchView()
+                    return true
+                } else {
+                    keyboardTaskFocusIndex = 0
+                }
+            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
+                // could potentially delay resuming launcher. See b/348668521 for more details.
+                touchInteractionService.startActivity(overviewComponentObserver.homeIntent)
+                return true
+            }
+            SHOW ->
+                // When Recents is not currently visible, the command's type is SHOW
+                // when overview is triggered via the keyboard overview button or Action+Tab
+                // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button
+                // nav is TYPE_TOGGLE.
+                keyboardTaskFocusIndex = 0
+            TOGGLE -> {}
+        }
+
+        recentsView?.setKeyboardTaskFocusIndex(keyboardTaskFocusIndex)
+        // Handle recents view focus when launching from home
+        val animatorListener: Animator.AnimatorListener =
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animation: Animator) {
+                    Log.d(TAG, "switching to Overview state - onAnimationStart: $command")
+                    super.onAnimationStart(animation)
+                    updateRecentsViewFocus(command)
+                    logShowOverviewFrom(command.type)
+                }
+
+                override fun onAnimationEnd(animation: Animator) {
+                    Log.d(TAG, "switching to Overview state - onAnimationEnd: $command")
+                    super.onAnimationEnd(animation)
+                    onRecentsViewFocusUpdated(command)
+                    onCallbackResult()
+                }
+            }
+        if (containerInterface.switchToRecentsIfVisible(animatorListener)) {
+            Log.d(TAG, "switching to Overview state - waiting: $command")
+            // If successfully switched, wait until animation finishes
+            return false
+        }
+
+        val activity = containerInterface.getCreatedContainer()
+        if (activity != null) {
+            InteractionJankMonitorWrapper.begin(activity.rootView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
+        }
+
+        val gestureState =
+            touchInteractionService.createGestureState(
+                GestureState.DEFAULT_STATE,
+                GestureState.TrackpadGestureType.NONE,
+            )
+        gestureState.isHandlingAtomicEvent = true
+        val interactionHandler =
+            touchInteractionService.swipeUpHandlerFactory.newHandler(
+                gestureState,
+                command.createTime,
+            )
+        interactionHandler.setGestureEndCallback {
+            onTransitionComplete(command, interactionHandler, onCallbackResult)
+        }
+        interactionHandler.initWhenReady("OverviewCommandHelper: command.type=${command.type}")
+
+        val recentAnimListener: RecentsAnimationCallbacks.RecentsAnimationListener =
+            object : RecentsAnimationCallbacks.RecentsAnimationListener {
+                override fun onRecentsAnimationStart(
+                    controller: RecentsAnimationController,
+                    targets: RecentsAnimationTargets,
+                ) {
+                    Log.d(TAG, "recents animation started: $command")
+                    updateRecentsViewFocus(command)
+                    logShowOverviewFrom(command.type)
+                    containerInterface.runOnInitBackgroundStateUI {
+                        Log.d(TAG, "recents animation started - onInitBackgroundStateUI: $command")
+                        interactionHandler.onGestureEnded(0f, PointF())
+                    }
+                    command.removeListener(this)
+                }
+
+                override fun onRecentsAnimationCanceled(
+                    thumbnailDatas: HashMap<Int, ThumbnailData>
+                ) {
+                    Log.d(TAG, "recents animation canceled: $command")
+                    interactionHandler.onGestureCancelled()
+                    command.removeListener(this)
+
+                    containerInterface.getCreatedContainer() ?: return
+                    recentsView?.onRecentsAnimationComplete()
+                }
+            }
+
+        if (taskAnimationManager.isRecentsAnimationRunning) {
+            command.setAnimationCallbacks(
+                taskAnimationManager.continueRecentsAnimation(gestureState)
+            )
+            command.addListener(interactionHandler)
+            taskAnimationManager.notifyRecentsAnimationState(interactionHandler)
+            interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/)
+
+            command.addListener(recentAnimListener)
+            taskAnimationManager.notifyRecentsAnimationState(recentAnimListener)
+        } else {
+            val intent =
+                Intent(interactionHandler.getLaunchIntent())
+                    .putExtra(ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID, gestureState.gestureId)
+            command.setAnimationCallbacks(
+                taskAnimationManager.startRecentsAnimation(gestureState, intent, interactionHandler)
+            )
+            interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/)
+            command.addListener(recentAnimListener)
+        }
+        Trace.beginAsyncSection(TRANSITION_NAME, 0)
+        Log.d(TAG, "switching via recents animation - onGestureStarted: $command")
+        return false
+    }
+
+    private fun onTransitionComplete(
+        command: CommandInfo,
+        handler: AbsSwipeUpHandler<*, *, *>,
+        onCommandResult: () -> Unit,
+    ) {
+        Log.d(TAG, "switching via recents animation - onTransitionComplete: $command")
+        command.removeListener(handler)
+        Trace.endAsyncSection(TRANSITION_NAME, 0)
+        onRecentsViewFocusUpdated(command)
+        onCommandResult()
+    }
+
+    /** Called when the command finishes execution. */
+    private fun onCommandFinished(command: CommandInfo) {
+        command.status = CommandStatus.COMPLETED
+        if (commandQueue.firstOrNull() !== command) {
+            Log.d(
+                TAG,
+                "next task not scheduled. First pending command type " +
+                    "is ${commandQueue.firstOrNull()} - command type is: $command",
+            )
+            return
+        }
+
+        Log.d(TAG, "command executed successfully! $command")
+        commandQueue.remove(command)
+        processNextCommand()
+    }
+
+    private fun cancelCommand(command: CommandInfo, throwable: Throwable?) {
+        command.status = CommandStatus.CANCELED
+        Log.e(TAG, "command cancelled: $command - $throwable")
+        commandQueue.remove(command)
+        processNextCommand()
+    }
+
+    private fun updateRecentsViewFocus(command: CommandInfo) {
+        val recentsView: RecentsView<*, *> = visibleRecentsView ?: return
+        if (command.type != KEYBOARD_INPUT && command.type != HIDE && command.type != SHOW) {
+            return
+        }
+
+        // When the overview is launched via alt tab (command type is TYPE_KEYBOARD_INPUT),
+        // the touch mode somehow is not change to false by the Android framework.
+        // The subsequent tab to go through tasks in overview can only be dispatched to
+        // focuses views, while focus can only be requested in
+        // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
+        // here we launch overview with live tile.
+        recentsView.viewRootImpl.touchModeChanged(false)
+        // Ensure that recents view has focus so that it receives the followup key inputs
+        // Stops requesting focused after first view gets focused.
+        recentsView.getTaskViewAt(keyboardTaskFocusIndex).requestFocus() ||
+            recentsView.nextTaskView.requestFocus() ||
+            recentsView.getTaskViewAt(0).requestFocus() ||
+            recentsView.requestFocus()
+    }
+
+    private fun onRecentsViewFocusUpdated(command: CommandInfo) {
+        val recentsView: RecentsView<*, *> = visibleRecentsView ?: return
+        if (command.type != HIDE || keyboardTaskFocusIndex == PagedView.INVALID_PAGE) {
+            return
+        }
+        recentsView.setKeyboardTaskFocusIndex(PagedView.INVALID_PAGE)
+        recentsView.currentPage = keyboardTaskFocusIndex
+        keyboardTaskFocusIndex = PagedView.INVALID_PAGE
+    }
+
+    private fun View?.requestFocus(): Boolean {
+        if (this == null) return false
+        post {
+            requestFocus()
+            requestAccessibilityFocus()
+        }
+        return true
+    }
+
+    private fun logShowOverviewFrom(commandType: CommandType) {
+        val container = containerInterface.getCreatedContainer() ?: return
+        val event =
+            when (commandType) {
+                SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
+                HIDE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
+                TOGGLE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
+                else -> return
+            }
+        StatsLogManager.newInstance(container.asContext())
+            .logger()
+            .withContainerInfo(
+                LauncherAtom.ContainerInfo.newBuilder()
+                    .setTaskSwitcherContainer(
+                        LauncherAtom.TaskSwitcherContainer.getDefaultInstance()
+                    )
+                    .build()
+            )
+            .log(event)
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.println("OverviewCommandHelper:")
+        pw.println("  pendingCommands=${commandQueue.size}")
+        if (commandQueue.isNotEmpty()) {
+            pw.println("    pendingCommandType=${commandQueue.first().type}")
+        }
+        pw.println("  keyboardTaskFocusIndex=$keyboardTaskFocusIndex")
+    }
+
+    @VisibleForTesting
+    data class CommandInfo(
+        val type: CommandType,
+        var status: CommandStatus = CommandStatus.IDLE,
+        val createTime: Long = SystemClock.elapsedRealtime(),
+        private var animationCallbacks: RecentsAnimationCallbacks? = null,
+    ) {
+        fun setAnimationCallbacks(recentsAnimationCallbacks: RecentsAnimationCallbacks) {
+            this.animationCallbacks = recentsAnimationCallbacks
+        }
+
+        fun addListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener) {
+            animationCallbacks?.addListener(listener)
+        }
+
+        fun removeListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener?) {
+            animationCallbacks?.removeListener(listener)
+        }
+
+        enum class CommandStatus {
+            IDLE,
+            PROCESSING,
+            COMPLETED,
+            CANCELED,
+        }
+    }
+
+    enum class CommandType {
+        SHOW,
+        KEYBOARD_INPUT,
+        HIDE,
+        TOGGLE, // Navigate to Overview
+        HOME, // Navigate to Home
+    }
+
+    companion object {
+        private const val TAG = "OverviewCommandHelper"
+        private const val TRANSITION_NAME = "Transition:toOverview"
+
+        /**
+         * Use case for needing a queue is double tapping recents button in 3 button nav. Size of 2
+         * should be enough. We'll toss in one more because we're kind hearted.
+         */
+        private const val MAX_QUEUE_SIZE = 3
+        private const val QUEUE_WAIT_DURATION_IN_MS = 5000L
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index d82426f..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,12 +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;
+    public BaseContainerInterface<?,?> getContainerInterface() {
+        return mContainerInterface;
     }
 
     public void dump(PrintWriter pw) {
@@ -300,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 b4b8c5b..2828a84 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -3,11 +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.graphics.Rect;
 import android.os.Bundle;
+import android.view.WindowInsets;
 
 import androidx.annotation.Nullable;
 
@@ -15,11 +14,11 @@
 import com.android.launcher3.testing.TestInformationHandler;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.DisplayController;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
@@ -77,26 +76,20 @@
                 return response;
             }
 
-            case TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET: {
-                if (!mDeviceProfile.isTablet) {
-                    return null;
-                }
-                Rect focusedTaskRect = new Rect();
-                LauncherActivityInterface.INSTANCE.calculateTaskSize(mContext, mDeviceProfile,
-                        focusedTaskRect, RecentsPagedOrientationHandler.PORTRAIT);
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, focusedTaskRect.height());
-                return response;
+            case TestProtocol.REQUEST_GET_OVERVIEW_TASK_SIZE: {
+                return getUIProperty(Bundle::putParcelable,
+                        recentsViewContainer ->
+                                recentsViewContainer.<RecentsView<?, ?>>getOverviewPanel()
+                                        .getLastComputedTaskSize(),
+                        this::getRecentsViewContainer);
             }
 
-            case TestProtocol.REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET: {
-                if (!mDeviceProfile.isTablet) {
-                    return null;
-                }
-                Rect gridTaskRect = new Rect();
-                LauncherActivityInterface.INSTANCE.calculateGridTaskSize(mContext, mDeviceProfile,
-                        gridTaskRect, RecentsPagedOrientationHandler.PORTRAIT);
-                response.putParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD, gridTaskRect);
-                return response;
+            case TestProtocol.REQUEST_GET_OVERVIEW_GRID_TASK_SIZE: {
+                return getUIProperty(Bundle::putParcelable,
+                        recentsViewContainer ->
+                                recentsViewContainer.<RecentsView<?, ?>>getOverviewPanel()
+                                        .getLastComputedGridTaskSize(),
+                        this::getRecentsViewContainer);
             }
 
             case TestProtocol.REQUEST_GET_OVERVIEW_PAGE_SPACING: {
@@ -210,11 +203,24 @@
     }
 
     @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();
+        }
+    }
+
+    private RecentsViewContainer getRecentsViewContainer() {
+        RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
+        OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
+        try {
+            return observer.getContainerInterface().getCreatedContainer();
         } finally {
             observer.onDestroy();
             rads.destroy();
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 3d4167a..714838a 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -20,13 +20,13 @@
 
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.wm.shell.util.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;
+import android.content.Context;
 import android.os.Process;
 import android.os.RemoteException;
 import android.util.SparseBooleanArray;
@@ -40,7 +40,8 @@
 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.util.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedTaskInfo;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -58,6 +59,7 @@
 
     private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
 
+    private final Context mContext;
     private final KeyguardManager mKeyguardManager;
     private final LooperExecutor mMainThreadExecutor;
     private final SystemUiProxy mSysUiProxy;
@@ -74,10 +76,12 @@
     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(LooperExecutor mainThreadExecutor, KeyguardManager keyguardManager,
-            SystemUiProxy sysUiProxy, TopTaskTracker topTaskTracker) {
+    public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
+            KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
+            TopTaskTracker topTaskTracker) {
+        mContext = context;
         mMainThreadExecutor = mainThreadExecutor;
         mKeyguardManager = keyguardManager;
         mChangeId = 1;
@@ -89,32 +93,40 @@
             }
 
             @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[] visibleTasks) {
                 mMainThreadExecutor.execute(() -> {
-                    topTaskTracker.onTaskMovedToFront(taskInfo);
+                    // TODO(b/346588978): We currently are only sending a single task, but this will
+                    //                    be updated once we send the full set of visible tasks
+                    final TaskInfo info = visibleTasks[0].getTaskInfo1();
+                    topTaskTracker.handleTaskMovedToFront(info);
                 });
             }
+
+            @Override
+            public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+                mMainThreadExecutor.execute(() -> topTaskTracker.onTaskChanged(taskInfo));
+            }
         });
         // We may receive onRunningTaskAppeared events later for tasks which have already been
         // included in the list returned by mSysUiProxy.getRunningTasks(), or may receive
@@ -241,7 +253,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);
@@ -250,13 +262,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;
             }
@@ -267,9 +279,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);
@@ -280,9 +292,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);
@@ -300,8 +312,12 @@
     @VisibleForTesting
     TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
         int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<GroupedRecentTaskInfo> rawTasks =
-                mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+        ArrayList<GroupedTaskInfo> rawTasks;
+        try {
+            rawTasks = mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+        } catch (SystemUiProxy.GetRecentTasksException e) {
+            return INVALID_RESULT;
+        }
         // The raw tasks are given in most-recent to least-recent order, we need to reverse it
         Collections.reverse(rawTasks);
 
@@ -319,11 +335,11 @@
         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 enableDesktopWindowingMode() is true,
+                // TYPE_FREEFORM tasks is only created when desktop mode can be entered,
                 // leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
-                if (enableDesktopWindowingMode()) {
+                if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
                     GroupTask desktopTask = createDesktopTask(rawTask);
                     if (desktopTask != null) {
                         allTasks.add(desktopTask);
@@ -331,14 +347,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
@@ -347,7 +362,6 @@
                         ? new Task(task2Key)
                         : Task.from(task2Key, taskInfo2,
                                 tmpLockedUsers.get(task2Key.userId) /* isLocked */);
-                task2.setLastSnapshotData(taskInfo2);
             } else {
                 // Is fullscreen task
                 if (numVisibleTasks > 0) {
@@ -371,17 +385,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;
@@ -416,10 +429,14 @@
         }
         writer.println(prefix + "  ]");
         int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<GroupedRecentTaskInfo> rawTasks =
-                mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+        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 18461a6..6075294 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -27,7 +27,6 @@
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
 import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -57,20 +56,23 @@
 import com.android.launcher3.LauncherAnimationRunner;
 import com.android.launcher3.LauncherAnimationRunner.AnimationResult;
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
+import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 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;
@@ -87,6 +89,7 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -100,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());
 
@@ -113,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
@@ -135,19 +135,19 @@
         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*/);
+        // Setup root and child views
         inflateRootView(R.layout.fallback_recents_activity);
-        setContentView(getRootView());
-        mDragLayer = findViewById(R.id.drag_layer);
-        mScrimView = findViewById(R.id.scrim_view);
-        mFallbackRecentsView = findViewById(R.id.overview_panel);
-        mActionsView = findViewById(R.id.overview_actions_view);
-        getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
-        mDragLayer.recreateControllers();
-        if (enableDesktopWindowingMode()) {
+        LauncherRootView rootView = getRootView();
+        mDragLayer = rootView.findViewById(R.id.drag_layer);
+        mScrimView = rootView.findViewById(R.id.scrim_view);
+        mFallbackRecentsView = rootView.findViewById(R.id.overview_panel);
+        mActionsView = rootView.findViewById(R.id.overview_actions_view);
+
+        if (DesktopModeStatus.canEnterDesktopMode(this)) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
                     null /* depthController */
@@ -156,6 +156,10 @@
         mFallbackRecentsView.init(mActionsView, mSplitSelectStateController,
                 mDesktopRecentsTransitionController);
 
+        setContentView(rootView);
+        rootView.getSysUiScrim().getSysUIProgress().updateValue(0);
+        mDragLayer.recreateControllers();
+
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
     }
 
@@ -171,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;
     }
 
@@ -192,7 +199,8 @@
     }
 
     @Override
-    protected void onHandleConfigurationChanged() {
+    public void onHandleConfigurationChanged() {
+        Trace.instant(Trace.TRACE_TAG_APP, "recentsActivity_onHandleConfigurationChanged");
         initDeviceProfile();
 
         AbstractFloatingView.closeOpenViews(this, true,
@@ -346,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();
@@ -419,7 +426,7 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
-        ACTIVITY_TRACKER.onActivityDestroyed(this);
+        ACTIVITY_TRACKER.onContextDestroyed(this);
         mActivityLaunchAnimationRunner = null;
         mSplitSelectStateController.onDestroy();
         mTISBindHelper.onDestroy();
@@ -512,6 +519,7 @@
     }
 
     @NonNull
+    @Override
     public TISBindHelper getTISBindHelper() {
         return mTISBindHelper;
     }
@@ -520,4 +528,10 @@
     public boolean isRecentsViewVisible() {
         return getStateManager().getState().isRecentsViewVisible();
     }
+
+    @Nullable
+    @Override
+    public DesktopVisibilityController getDesktopVisibilityController() {
+        return mTISBindHelper.getDesktopVisibilityController();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index da7a98f..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;
 
@@ -54,17 +53,14 @@
 
     private final Set<RecentsAnimationListener> mListeners = new ArraySet<>();
     private final SystemUiProxy mSystemUiProxy;
-    private final boolean mAllowMinimizeSplitScreen;
 
     // TODO(141886704): Remove these references when they are no longer needed
     private RecentsAnimationController mController;
 
     private boolean mCancelled;
 
-    public RecentsAnimationCallbacks(SystemUiProxy systemUiProxy,
-            boolean allowMinimizeSplitScreen) {
+    public RecentsAnimationCallbacks(SystemUiProxy systemUiProxy) {
         mSystemUiProxy = systemUiProxy;
-        mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
     }
 
     @UiThread
@@ -109,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 */);
@@ -122,21 +120,17 @@
         }
 
         mController = new RecentsAnimationController(animationController,
-                mAllowMinimizeSplitScreen, this::onAnimationFinished);
+                this::onAnimationFinished);
         if (mCancelled) {
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(),
                     mController::finishAnimationToApp);
         } else {
             RemoteAnimationTarget[] nonAppTargets;
-            if (!TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-                nonAppTargets = mSystemUiProxy.onGoingToRecentsLegacy(appTargets);
-            } else {
-                final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
-                final ArrayList<RemoteAnimationTarget> nonApps = new ArrayList<>();
-                classifyTargets(appTargets, apps, nonApps);
-                appTargets = apps.toArray(new RemoteAnimationTarget[apps.size()]);
-                nonAppTargets = nonApps.toArray(new RemoteAnimationTarget[nonApps.size()]);
-            }
+            final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
+            final ArrayList<RemoteAnimationTarget> nonApps = new ArrayList<>();
+            classifyTargets(appTargets, apps, nonApps);
+            appTargets = apps.toArray(new RemoteAnimationTarget[apps.size()]);
+            nonAppTargets = nonApps.toArray(new RemoteAnimationTarget[nonApps.size()]);
             if (nonAppTargets == null) {
                 nonAppTargets = new RemoteAnimationTarget[0];
             }
@@ -145,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);
                 }
@@ -160,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);
             }
@@ -173,31 +162,16 @@
     @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);
             }
         });
     }
 
-    @BinderThread
-    @Override
-    public boolean onSwitchToScreenshot(Runnable onFinished) {
+    private void onAnimationFinished(RecentsAnimationController controller) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-            for (RecentsAnimationListener listener : getListeners()) {
-                if (listener.onSwitchToScreenshot(onFinished)) return;
-            }
-            onFinished.run();
-        });
-        return true;
-    }
-
-    private final 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);
             }
@@ -223,7 +197,6 @@
     public void dump(String prefix, PrintWriter pw) {
         pw.println(prefix + "RecentsAnimationCallbacks:");
 
-        pw.println(prefix + "\tmAllowMinimizeSplitScreen=" + mAllowMinimizeSplitScreen);
         pw.println(prefix + "\tmCancelled=" + mCancelled);
     }
 
@@ -250,12 +223,5 @@
          * Callback made when a task started from the recents is ready for an app transition.
          */
         default void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTarget) {}
-
-        /**
-         * @return whether this will call onFinished or not (onFinished should only be called once).
-         */
-        default boolean onSwitchToScreenshot(Runnable onFinished) {
-            return false;
-        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 1b05e28..dcb0108 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -17,14 +17,10 @@
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
 
-import android.content.Context;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Log;
-import android.view.IRecentsAnimationController;
 import android.view.SurfaceControl;
 import android.view.WindowManagerGlobal;
 import android.window.PictureInPictureSurfaceTransaction;
@@ -35,11 +31,11 @@
 import com.android.internal.os.IResultReceiver;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
-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.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.wm.shell.recents.IRecentsAnimationController;
 
 import java.io.PrintWriter;
 import java.util.function.Consumer;
@@ -52,21 +48,17 @@
     private static final String TAG = "RecentsAnimationController";
     private final RecentsAnimationControllerCompat mController;
     private final Consumer<RecentsAnimationController> mOnFinishedListener;
-    private final boolean mAllowMinimizeSplitScreen;
 
     private boolean mUseLauncherSysBarFlags = false;
-    private boolean mSplitScreenMinimized = false;
     private boolean mFinishRequested = false;
     // Only valid when mFinishRequested == true.
     private boolean mFinishTargetIsLauncher;
     private RunnableList mPendingFinishCallbacks = new RunnableList();
 
     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
-            boolean allowMinimizeSplitScreen,
             Consumer<RecentsAnimationController> onFinishedListener) {
         mController = controller;
         mOnFinishedListener = onFinishedListener;
-        mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
     }
 
     /**
@@ -85,42 +77,16 @@
         if (mUseLauncherSysBarFlags != useLauncherSysBarFlags) {
             mUseLauncherSysBarFlags = useLauncherSysBarFlags;
             UI_HELPER_EXECUTOR.execute(() -> {
-                if (!ENABLE_SHELL_TRANSITIONS) {
-                    mController.setAnimationTargetsBehindSystemBars(!useLauncherSysBarFlags);
-                } else {
-                    try {
-                        WindowManagerGlobal.getWindowManagerService().setRecentsAppBehindSystemBars(
-                                useLauncherSysBarFlags);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Unable to reach window manager", e);
-                    }
+                try {
+                    WindowManagerGlobal.getWindowManagerService().setRecentsAppBehindSystemBars(
+                            useLauncherSysBarFlags);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Unable to reach window manager", e);
                 }
             });
         }
     }
 
-    /**
-     * Indicates that the gesture has crossed the window boundary threshold and we should minimize
-     * if we are in splitscreen.
-     */
-    public void setSplitScreenMinimized(Context context, boolean splitScreenMinimized) {
-        if (!mAllowMinimizeSplitScreen) {
-            return;
-        }
-        if (mSplitScreenMinimized != splitScreenMinimized) {
-            mSplitScreenMinimized = splitScreenMinimized;
-        }
-    }
-
-    /**
-     * Remove task remote animation target from
-     * {@link RecentsAnimationCallbacks#onTasksAppeared}}.
-     */
-    @UiThread
-    public void removeTaskTarget(int targetTaskId) {
-        UI_HELPER_EXECUTOR.execute(() -> mController.removeTask(targetTaskId));
-    }
-
     @UiThread
     public void finishAnimationToHome() {
         finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
@@ -165,10 +131,7 @@
             // trigger the callback to be called immediately
             return;
         }
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "finishRecentsAnimation",
-                /* extras= */ toRecents,
-                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
+        ActiveGestureProtoLogProxy.logFinishRecentsAnimation(toRecents);
         // Finish not yet requested
         mFinishRequested = true;
         mFinishTargetIsLauncher = toRecents;
@@ -177,7 +140,7 @@
             mController.finish(toRecents, sendUserLeaveHint, new IResultReceiver.Stub() {
                 @Override
                 public void send(int i, Bundle bundle) throws RemoteException {
-                    ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation-callback");
+                    ActiveGestureProtoLogProxy.logFinishRecentsAnimationCallback();
                     MAIN_EXECUTOR.execute(() -> {
                         mPendingFinishCallbacks.executeAllAndDestroy();
                     });
@@ -195,19 +158,6 @@
     }
 
     /**
-     * @see IRecentsAnimationController#cleanupScreenshot()
-     */
-    @UiThread
-    public void cleanupScreenshot() {
-        UI_HELPER_EXECUTOR.execute(() -> {
-            ActiveGestureLog.INSTANCE.addLog(
-                    "cleanupScreenshot",
-                    ActiveGestureErrorDetector.GestureEvent.CLEANUP_SCREENSHOT);
-            mController.cleanupScreenshot();
-        });
-    }
-
-    /**
      * @see RecentsAnimationControllerCompat#detachNavigationBarFromApp
      */
     @UiThread
@@ -216,14 +166,6 @@
     }
 
     /**
-     * @see IRecentsAnimationController#animateNavigationBarToApp(long)
-     */
-    @UiThread
-    public void animateNavigationBarToApp(long duration) {
-        UI_HELPER_EXECUTOR.execute(() -> mController.animateNavigationBarToApp(duration));
-    }
-
-    /**
      * @see IRecentsAnimationController#setWillFinishToHome(boolean)
      */
     @UiThread
@@ -272,9 +214,7 @@
     public void dump(String prefix, PrintWriter pw) {
         pw.println(prefix + "RecentsAnimationController:");
 
-        pw.println(prefix + "\tmAllowMinimizeSplitScreen=" + mAllowMinimizeSplitScreen);
         pw.println(prefix + "\tmUseLauncherSysBarFlags=" + mUseLauncherSysBarFlags);
-        pw.println(prefix + "\tmSplitScreenMinimized=" + mSplitScreenMinimized);
         pw.println(prefix + "\tmFinishRequested=" + mFinishRequested);
         pw.println(prefix + "\tmFinishTargetIsLauncher=" + mFinishTargetIsLauncher);
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index cd62265..de8be50 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;
@@ -33,6 +34,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -69,10 +71,11 @@
 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;
+import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -99,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;
@@ -150,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) {
@@ -416,7 +419,8 @@
                         | SYSUI_STATE_QUICK_SETTINGS_EXPANDED
                         | SYSUI_STATE_MAGNIFICATION_OVERLAP
                         | SYSUI_STATE_DEVICE_DREAMING
-                        | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+                        | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+                        | SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
         return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
     }
 
@@ -546,6 +550,13 @@
     }
 
     /**
+     * @return whether the Assistant gesture can be used in 3 button navigation mode.
+     */
+    public boolean supportsAssistantGestureInButtonNav() {
+        return Flags.threeButtonCornerSwipe();
+    }
+
+    /**
      * @param ev An ACTION_DOWN motion event
      * @return whether the given motion event can trigger the assistant over the current task.
      */
@@ -553,6 +564,7 @@
         return mAssistantAvailable
                 && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
                 && mRotationTouchHelper.touchInAssistantRegion(ev)
+                && !isTrackpadScroll(ev)
                 && !isLockToAppActive();
     }
 
@@ -607,8 +619,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/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index 82bb453..cf7e499 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -18,13 +18,14 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-
 import android.app.WindowConfiguration;
+import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.RemoteAnimationTarget;
 
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+
 import java.io.PrintWriter;
 
 /**
@@ -54,8 +55,8 @@
      *
      * @return {@code true} if at least one target app is a desktop task
      */
-    public boolean hasDesktopTasks() {
-        if (!enableDesktopWindowingMode()) {
+    public boolean hasDesktopTasks(Context context) {
+        if (!DesktopModeStatus.canEnterDesktopMode(context)) {
             return false;
         }
         for (RemoteAnimationTarget target : apps) {
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index b796951..d073580 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -18,6 +18,7 @@
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 
@@ -42,6 +43,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.recents.data.RecentTasksDataSource;
+import com.android.quickstep.recents.data.TaskVisualsChangeNotifier;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TaskVisualsChangeListener;
@@ -54,6 +56,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.function.Consumer;
@@ -64,7 +67,8 @@
  */
 @TargetApi(Build.VERSION_CODES.O)
 public class RecentsModel implements RecentTasksDataSource, IconChangeListener,
-        TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable {
+        TaskStackChangeListener, TaskVisualsChangeListener, TaskVisualsChangeNotifier,
+        SafeCloseable {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
@@ -73,7 +77,8 @@
     private static final Executor RECENTS_MODEL_EXECUTOR = Executors.newSingleThreadExecutor(
             new SimpleThreadFactory("TaskThumbnailIconCache-", THREAD_PRIORITY_BACKGROUND));
 
-    private final List<TaskVisualsChangeListener> mThumbnailChangeListeners = new ArrayList<>();
+    private final ConcurrentLinkedQueue<TaskVisualsChangeListener> mThumbnailChangeListeners =
+            new ConcurrentLinkedQueue<>();
     private final Context mContext;
 
     private final RecentTasksList mTaskList;
@@ -89,7 +94,9 @@
 
     private RecentsModel(Context context, IconProvider iconProvider) {
         this(context,
-                new RecentTasksList(MAIN_EXECUTOR,
+                new RecentTasksList(
+                        context,
+                        MAIN_EXECUTOR,
                         context.getSystemService(KeyguardManager.class),
                         SystemUiProxy.INSTANCE.get(context),
                         TopTaskTracker.INSTANCE.get(context)),
@@ -108,7 +115,7 @@
         mIconCache = iconCache;
         mIconCache.registerTaskVisualsChangeListener(this);
         mThumbnailCache = thumbnailCache;
-        if (enableGridOnlyOverview()) {
+        if (isCachePreloadingEnabled()) {
             mCallbacks = new ComponentCallbacks() {
                 @Override
                 public void onConfigurationChanged(Configuration configuration) {
@@ -234,8 +241,8 @@
     public boolean onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
         mThumbnailCache.updateTaskSnapShot(taskId, snapshot);
 
-        for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
-            Task task = mThumbnailChangeListeners.get(i).onTaskThumbnailChanged(taskId, snapshot);
+        for (TaskVisualsChangeListener listener : mThumbnailChangeListeners) {
+            Task task = listener.onTaskThumbnailChanged(taskId, snapshot);
             if (task != null) {
                 task.thumbnail = snapshot;
             }
@@ -264,8 +271,8 @@
     @Override
     public void onAppIconChanged(String packageName, UserHandle user) {
         mIconCache.invalidateCacheEntries(packageName, user);
-        for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
-            mThumbnailChangeListeners.get(i).onTaskIconChanged(packageName, user);
+        for (TaskVisualsChangeListener listener : mThumbnailChangeListeners) {
+            listener.onTaskIconChanged(packageName, user);
         }
     }
 
@@ -284,6 +291,7 @@
     /**
      * Adds a listener for visuals changes
      */
+    @Override
     public void addThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.add(listener);
     }
@@ -291,6 +299,7 @@
     /**
      * Removes a previously added listener
      */
+    @Override
     public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.remove(listener);
     }
@@ -342,7 +351,7 @@
      * highResLoadingState is enabled
      */
     public void preloadCacheIfNeeded() {
-        if (!enableGridOnlyOverview()) {
+        if (!isCachePreloadingEnabled()) {
             return;
         }
 
@@ -368,7 +377,7 @@
      * Updates cache size and preloads more tasks if cache size increases
      */
     public void updateCacheSizeAndPreloadIfNeeded() {
-        if (!enableGridOnlyOverview()) {
+        if (!isCachePreloadingEnabled()) {
             return;
         }
 
@@ -387,6 +396,10 @@
         mTaskStackChangeListeners.unregisterTaskStackListener(this);
     }
 
+    private boolean isCachePreloadingEnabled() {
+        return enableGridOnlyOverview() || enableRefactorTaskThumbnail();
+    }
+
     /**
      * Listener for receiving running tasks changes
      */
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 3dec381..06b2972 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -17,7 +17,7 @@
 package com.android.quickstep;
 
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.wm.shell.util.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
+import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
 
 import android.app.WindowConfiguration;
 import android.content.Context;
@@ -33,7 +33,7 @@
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
-import com.android.wm.shell.util.SplitBounds;
+import com.android.wm.shell.shared.split.SplitBounds;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -68,7 +68,7 @@
      */
     public RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy) {
         DesktopVisibilityController desktopVisibilityController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+                sizingStrategy.getDesktopVisibilityController();
         if (desktopVisibilityController != null) {
             int visibleTasksCount = desktopVisibilityController.getVisibleDesktopTasksCount();
             if (visibleTasksCount > 0) {
@@ -94,7 +94,7 @@
         RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
         for (int i = 0; i < numHandles; i++) {
             TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy);
-            tvs.setIsDesktopTask(forDesktop);
+            tvs.setIsDesktopTask(forDesktop , i);
             TransformParams transformParams = new TransformParams();
             handles[i] = new RemoteTargetHandle(tvs, transformParams);
         }
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 6f1ab7d..79abc0f 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -157,7 +158,7 @@
         // Register for navigation mode changes
         mDisplayController.addChangeListener(this);
         DisplayController.Info info = mDisplayController.getInfo();
-        onDisplayInfoChangedInternal(info, CHANGE_ALL, info.getNavigationMode().hasGestures);
+        onDisplayInfoChangedInternal(info, CHANGE_ALL, hasGestures(info.getNavigationMode()));
         runOnDestroy(() -> mDisplayController.removeChangeListener(this));
 
         mOrientationListener = new OrientationEventListener(mContext) {
@@ -179,6 +180,7 @@
                 }
             }
         };
+        runOnDestroy(() -> mOrientationListener.disable());
         mNeedsInit = false;
     }
 
@@ -211,6 +213,7 @@
             r.run();
         }
         mNeedsInit = true;
+        mOnDestroyActions.clear();
     }
 
     public boolean isTaskListFrozen() {
@@ -229,7 +232,7 @@
      * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
      */
     public void updateGestureTouchRegions() {
-        if (!mMode.hasGestures) {
+        if (!hasGestures(mMode)) {
             return;
         }
 
@@ -268,7 +271,7 @@
                 | CHANGE_SUPPORTED_BOUNDS)) != 0) {
             mDisplayRotation = info.rotation;
 
-            if (mMode.hasGestures) {
+            if (hasGestures(mMode)) {
                 updateGestureTouchRegions();
                 mOrientationTouchTransformer.createOrAddTouchRegion(info);
                 mCurrentAppRotation = mDisplayRotation;
@@ -295,9 +298,9 @@
             mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
                     mContext.getResources());
 
-            if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) {
+            if (forceRegister || (!hasGestures(mMode) && hasGestures(newMode))) {
                 setupOrientationSwipeHandler();
-            } else if (mMode.hasGestures && !newMode.hasGestures) {
+            } else if (hasGestures(mMode) && !hasGestures(newMode)) {
                 destroyOrientationSwipeHandlerCallback();
             }
 
@@ -399,7 +402,7 @@
     }
 
     public int getCurrentActiveRotation() {
-        if (!mMode.hasGestures) {
+        if (!hasGestures(mMode)) {
             // touch rotation should always match that of display for 3 button
             return mDisplayRotation;
         }
@@ -416,4 +419,8 @@
     public OrientationTouchTransformer getOrientationTouchTransformer() {
         return mOrientationTouchTransformer;
     }
+
+    private boolean hasGestures(NavigationMode mode) {
+        return mode.hasGestures || (mode == THREE_BUTTONS && Flags.threeButtonCornerSwipe());
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
index 29a57fc..5264643 100644
--- a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
@@ -34,12 +34,18 @@
 
     private final Context mContext;
     private OrientationRectF mOrientationRectF;
+    private OrientationRectF mTouchingOrientationRectF;
+    private int mViewRotation;
 
     public SimpleOrientationTouchTransformer(Context context) {
+        this(context, DisplayController.INSTANCE.get(context));
+    }
+
+    @androidx.annotation.VisibleForTesting
+    public SimpleOrientationTouchTransformer(Context context, DisplayController displayController) {
         mContext = context;
-        DisplayController.INSTANCE.get(context).addChangeListener(this);
-        onDisplayInfoChanged(context, DisplayController.INSTANCE.get(context).getInfo(),
-                CHANGE_ALL);
+        displayController.addChangeListener(this);
+        onDisplayInfoChanged(context, displayController.getInfo(), CHANGE_ALL);
     }
 
     @Override
@@ -56,7 +62,29 @@
                 info.rotation);
     }
 
+    /**
+     * Called when the touch is started. This preserves the touching orientation until the touch is
+     * done (i.e. ACTION_CANCEL or ACTION_UP). So the transform won't produce inconsistent position
+     * if display is changed during the touch.
+     */
+    public void updateTouchingOrientation(int viewRotation) {
+        mViewRotation = viewRotation;
+        mTouchingOrientationRectF = new OrientationRectF(mOrientationRectF.left,
+                mOrientationRectF.top, mOrientationRectF.right, mOrientationRectF.bottom,
+                mOrientationRectF.getRotation());
+    }
+
+    /** Called when the touch is finished. */
+    public void clearTouchingOrientation() {
+        mTouchingOrientationRectF = null;
+    }
+
     public void transform(MotionEvent ev, int rotation) {
+        if (mTouchingOrientationRectF != null) {
+            mTouchingOrientationRectF.applyTransformToRotation(ev, mViewRotation,
+                    true /* forceTransform */);
+            return;
+        }
         mOrientationRectF.applyTransformToRotation(ev, rotation, true /* forceTransform */);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 0d9f81f..6c4c74c 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -21,10 +21,7 @@
 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 static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -45,13 +42,12 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
-import android.view.IRecentsAnimationController;
-import android.view.IRecentsAnimationRunner;
 import android.view.IRemoteAnimationRunner;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
+import android.window.DesktopModeFlags;
 import android.window.IOnBackInvokedCallback;
 import android.window.RemoteTransition;
 import android.window.TaskSnapshot;
@@ -64,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;
@@ -84,24 +82,28 @@
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.bubbles.IBubbles;
 import com.android.wm.shell.bubbles.IBubblesListener;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.common.pip.IPip;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
-import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
 import com.android.wm.shell.desktopmode.IDesktopMode;
 import com.android.wm.shell.desktopmode.IDesktopTaskListener;
 import com.android.wm.shell.draganddrop.IDragAndDrop;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.recents.IRecentTasks;
 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.GroupedTaskInfo;
 import com.android.wm.shell.shared.IShellTransitions;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
+import com.android.wm.shell.shared.split.SplitBounds;
+import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
 import com.android.wm.shell.splitscreen.ISplitScreen;
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
 import com.android.wm.shell.splitscreen.ISplitSelectListener;
 import com.android.wm.shell.startingsurface.IStartingWindow;
 import com.android.wm.shell.startingsurface.IStartingWindowListener;
-import com.android.wm.shell.util.GroupedRecentTaskInfo;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -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);
             }
@@ -240,6 +244,17 @@
     }
 
     @Override
+    public void updateContextualEduStats(boolean isTrackpadGesture, String gestureType) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.updateContextualEduStats(isTrackpadGesture, gestureType);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call updateContextualEduStats");
+            }
+        }
+    }
+
+    @Override
     public void setHomeRotationEnabled(boolean enabled) {
         if (mSystemUiProxy != null) {
             try {
@@ -288,6 +303,7 @@
         registerSplitScreenListener(mSplitScreenListener);
         registerSplitSelectListener(mSplitSelectListener);
         mHomeVisibilityState.init(mShellTransitions);
+        mFocusState.init(mShellTransitions);
         setStartingWindowListener(mStartingWindowListener);
         setLauncherUnlockAnimationController(
                 mLauncherActivityClass, mLauncherUnlockAnimationController);
@@ -297,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) {
@@ -842,12 +858,14 @@
 
     /**
      * Tells SysUI to dismiss the bubble with the provided key.
+     *
      * @param key the key of the bubble to dismiss.
+     * @param timestamp the timestamp when the removal happened.
      */
-    public void dragBubbleToDismiss(String key) {
+    public void dragBubbleToDismiss(String key, long timestamp) {
         if (mBubbles == null) return;
         try {
-            mBubbles.dragBubbleToDismiss(key);
+            mBubbles.dragBubbleToDismiss(key, timestamp);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed call dragBubbleToDismiss");
         }
@@ -868,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");
         }
@@ -892,6 +912,47 @@
         }
     }
 
+    /**
+     * Tells SysUI to show a shortcut bubble.
+     *
+     * @param info the shortcut info used to create or identify the bubble.
+     */
+    public void showShortcutBubble(ShortcutInfo info) {
+        try {
+            if (mBubbles != null) {
+                mBubbles.showShortcutBubble(info);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call show bubble for shortcut");
+        }
+    }
+
+    /**
+     * Tells SysUI to show a bubble of an app.
+     *
+     * @param intent the intent used to create the bubble.
+     */
+    public void showAppBubble(Intent intent) {
+        try {
+            if (mBubbles != null) {
+                mBubbles.showAppBubble(intent);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call show bubble for app");
+        }
+    }
+
+    /** Tells SysUI to show the expanded view. */
+    public void showExpandedView() {
+        try {
+            if (mBubbles != null) {
+                mBubbles.showExpandedView();
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call showExpandedView");
+        }
+    }
+
     //
     // Splitscreen
     //
@@ -998,77 +1059,6 @@
         }
     }
 
-    /**
-     * Start multiple tasks in split-screen simultaneously.
-     */
-    public void startTasksWithLegacyTransition(int taskId1, Bundle options1, int taskId2,
-            Bundle options2, @StagePosition int splitPosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startTasksWithLegacyTransition(taskId1, options1, taskId2, options2,
-                        splitPosition, snapPosition, adapter, instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startTasksWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
-    public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1,
-            Bundle options1, int taskId, Bundle options2, @StagePosition int splitPosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startIntentAndTaskWithLegacyTransition(pendingIntent, userId1,
-                        options1, taskId, options2, splitPosition, snapPosition, adapter,
-                        instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startIntentAndTaskWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
-    public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, Bundle options1,
-            int taskId, Bundle options2, @StagePosition int splitPosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startShortcutAndTaskWithLegacyTransition(shortcutInfo, options1,
-                        taskId, options2, splitPosition, snapPosition, adapter, instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startShortcutAndTaskWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
-    /**
-     * Starts a pair of intents or shortcuts in split-screen using legacy transition. Passing a
-     * non-null shortcut info means to start the app as a shortcut.
-     */
-    public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1,
-            @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
-            PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2,
-            @Nullable Bundle options2, @StagePosition int sidePosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startIntentsWithLegacyTransition(pendingIntent1, userId1,
-                        shortcutInfo1, options1, pendingIntent2, userId2, shortcutInfo2, options2,
-                        sidePosition, snapPosition, adapter, instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startIntentsWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
     public void startShortcut(String packageName, String shortcutId, int position,
             Bundle options, UserHandle user, InstanceId instanceId) {
         if (mSplitScreen != null) {
@@ -1093,46 +1083,6 @@
         }
     }
 
-    public void removeFromSideStage(int taskId) {
-        if (mSplitScreen != null) {
-            try {
-                mSplitScreen.removeFromSideStage(taskId);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call removeFromSideStage");
-            }
-        }
-    }
-
-    /**
-     * Call this when going to recents so that shell can set-up and provide appropriate leashes
-     * for animation (eg. DividerBar).
-     *
-     * @return RemoteAnimationTargets of windows that need to animate but only exist in shell.
-     */
-    @Nullable
-    public RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) {
-        if (!TaskAnimationManager.ENABLE_SHELL_TRANSITIONS && mSplitScreen != null) {
-            try {
-                return mSplitScreen.onGoingToRecentsLegacy(apps);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call onGoingToRecentsLegacy");
-            }
-        }
-        return null;
-    }
-
-    @Nullable
-    public RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) {
-        if (mSplitScreen != null) {
-            try {
-                return mSplitScreen.onStartingSplitLegacy(apps);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call onStartingSplitLegacy");
-            }
-        }
-        return null;
-    }
-
     //
     // One handed
     //
@@ -1190,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
@@ -1392,21 +1346,37 @@
         }
     }
 
-    public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {
+    public static class GetRecentTasksException extends Exception {
+        public GetRecentTasksException(String message) {
+            super(message);
+        }
+
+        public GetRecentTasksException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     * Retrieves a list of Recent tasks from ActivityManager.
+     * @throws GetRecentTasksException if IRecentTasks is not initialized, or when we get
+     * RemoteException from server side
+     */
+    public ArrayList<GroupedTaskInfo> getRecentTasks(int numTasks,
+            int userId) throws GetRecentTasksException {
         if (mRecentTasks == null) {
-            Log.w(TAG, "getRecentTasks() failed due to null mRecentTasks");
-            return new ArrayList<>();
+            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<>();
             }
             return new ArrayList<>(Arrays.asList(rawTasks));
         } catch (RemoteException e) {
-            Log.w(TAG, "Failed call getRecentTasks", e);
-            return new ArrayList<>();
+            Log.e(TAG, "Failed call getRecentTasks", e);
+            throw new GetRecentTasksException("Failed call getRecentTasks", e);
         }
     }
 
@@ -1425,8 +1395,8 @@
     }
 
     private boolean shouldEnableRunningTasksForDesktopMode() {
-        // TODO(b/335401172): unify DesktopMode checks in Launcher
-        return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
+        return DesktopModeStatus.canEnterDesktopMode(mContext)
+                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue();
     }
 
     private boolean handleMessageAsync(Message msg) {
@@ -1460,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);
             }
@@ -1516,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
     //
@@ -1547,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() {
@@ -1560,7 +1552,7 @@
                 // Aidl bundles need to explicitly set class loader
                 // https://developer.android.com/guide/components/aidl#Bundles
                 if (extras != null) {
-                    extras.setClassLoader(getClass().getClassLoader());
+                    extras.setClassLoader(SplitBounds.class.getClassLoader());
                 }
                 listener.onAnimationStart(new RecentsAnimationControllerCompat(controller), apps,
                         wallpapers, homeContentInsets, minimizedHomeBounds, extras);
@@ -1578,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);
@@ -1622,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 08bb6cd..0ea128a 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -18,6 +18,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 
 import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
+import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 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.NavigationMode.NO_BUTTON;
@@ -25,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;
 
@@ -42,10 +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;
@@ -58,11 +61,11 @@
 import java.util.HashMap;
 
 public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener {
-    public static final boolean ENABLE_SHELL_TRANSITIONS = true;
-    public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS
-            && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
+    public static final boolean SHELL_TRANSITIONS_ROTATION =
+            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;
@@ -97,10 +100,10 @@
         }
     };
 
-    TaskAnimationManager(Context ctx) {
+    TaskAnimationManager(Context ctx, RecentsWindowManager manager) {
         mCtx = ctx;
+        mRecentsWindowsManager = manager;
     }
-
     SystemUiProxy getSystemUiProxy() {
         return SystemUiProxy.INSTANCE.get(mCtx);
     }
@@ -110,8 +113,9 @@
      */
     public void preloadRecentsAnimation(Intent intent) {
         // Pass null animation handler to indicate this start is for preloading
-        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
-                .startRecentsActivity(intent, 0, null, null, null));
+        UI_HELPER_EXECUTOR.execute(() -> {
+            ActivityManagerWrapper.getInstance().preloadRecentsActivity(intent);
+        });
     }
 
     boolean shouldIgnoreMotionEvents() {
@@ -132,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";
@@ -157,17 +159,15 @@
 
         final BaseContainerInterface containerInterface = gestureState.getContainerInterface();
         mLastGestureState = gestureState;
-        RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks(
-                getSystemUiProxy(), containerInterface.allowMinimizeSplitScreen());
+        RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks(getSystemUiProxy());
         mCallbacks = newCallbacks;
         mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
             @Override
             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) {
@@ -188,7 +188,7 @@
                 }
                 mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
 
-                if (ENABLE_SHELL_TRANSITIONS && mTargets.hasRecents
+                if (mTargets.hasRecents
                         // The filtered (MODE_CLOSING) targets only contain 1 home activity.
                         && mTargets.apps.length == 1
                         && mTargets.apps[0].windowConfiguration.getActivityType()
@@ -201,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 */);
                         }
                     });
                 }
@@ -211,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);
@@ -223,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);
@@ -264,12 +259,7 @@
                     }
                 }
 
-                RemoteAnimationTarget[] nonAppTargets = ENABLE_SHELL_TRANSITIONS
-                        ? null : getSystemUiProxy().onStartingSplitLegacy(
-                                appearedTaskTargets);
-                if (nonAppTargets == null) {
-                    nonAppTargets = new RemoteAnimationTarget[0];
-                }
+                RemoteAnimationTarget[] nonAppTargets = new RemoteAnimationTarget[0];
                 if ((containerInterface.isInLiveTileMode()
                             || mLastGestureState.getEndTarget() == RECENTS
                             || isNonRecentsStartedTasksAppeared(appearedTaskTargets))
@@ -277,93 +267,72 @@
                     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 */,
                             true /*shown*/, null /* animatorHandler */);
                 }
                 if (mController != null) {
-                    if (mLastAppearedTaskTargets != null) {
-                        for (RemoteAnimationTarget lastTarget : mLastAppearedTaskTargets) {
-                            for (RemoteAnimationTarget appearedTarget : appearedTaskTargets) {
-                                if (lastTarget != null &&
-                                        appearedTarget.taskId != lastTarget.taskId) {
-                                    mController.removeTaskTarget(lastTarget.taskId);
-                                }
-                            }
-                        }
-                    }
                     mLastAppearedTaskTargets = appearedTaskTargets;
                     mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
                 }
             }
-
-            @Override
-            public boolean onSwitchToScreenshot(Runnable onFinished) {
-                if (!containerInterface.isInLiveTileMode()
-                        || containerInterface.getCreatedContainer() == null) {
-                    // No need to switch since tile is already a screenshot.
-                    onFinished.run();
-                } else {
-                    final RecentsView recentsView =
-                            containerInterface.getCreatedContainer().getOverviewPanel();
-                    if (recentsView != null) {
-                        recentsView.switchToScreenshot(onFinished);
-                    } else {
-                        onFinished.run();
-                    }
-                }
-                return true;
-            }
         });
         final long eventTime = gestureState.getSwipeUpStartTimeMs();
         mCallbacks.addListener(gestureState);
         mCallbacks.addListener(listener);
 
-        if (ENABLE_SHELL_TRANSITIONS) {
-            final ActivityOptions options = ActivityOptions.makeBasic();
-            options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
-            // Use regular (non-transient) launch for all apps page to control IME.
-            if (!containerInterface.allowAllAppsFromOverview()) {
-                options.setTransientLaunch();
-            }
-            options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
-            mRecentsAnimationStartPending = getSystemUiProxy()
-                    .startRecentsActivity(intent, options, mCallbacks);
-            if (enableHandleDelayedGestureCallbacks()) {
-                ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                        "TaskAnimationManager.startRecentsAnimation(shell transition path): ")
-                        .append("Setting mRecentsAnimationStartPending = ")
-                        .append(mRecentsAnimationStartPending));
-            }
+        final ActivityOptions options = ActivityOptions.makeBasic();
+
+        // 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 {
-            UI_HELPER_EXECUTOR.execute(
-                    () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
-                            intent,
-                            eventTime,
-                            mCallbacks,
-                            result -> {
-                                if (enableHandleDelayedGestureCallbacks()) {
-                                    ActiveGestureLog.INSTANCE.addLog(
-                                            new ActiveGestureLog.CompoundString(
-                                                    "TaskAnimationManager.startRecentsAnimation")
-                                                    .append("(legacy path): Setting ")
-                                                    .append("mRecentsAnimationStartPending = ")
-                                                    .append(result));
-                                }
-                                mRecentsAnimationStartPending = result;
-                            },
-                            MAIN_EXECUTOR.getHandler()));
+            options.setPendingIntentBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+            options.setTransientLaunch();
+            options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
+
+            // Notify taskbar that we should skip reacting to launcher visibility change to
+            // avoid a jumping taskbar.
+            TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+            if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+                taskbarUIController.setSkipLauncherVisibilityChange(true);
+
+                mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+                    @Override
+                    public void onRecentsAnimationCanceled(
+                            @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+
+                    @Override
+                    public void onRecentsAnimationFinished(
+                            @NonNull RecentsAnimationController controller) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+                });
+            }
+
+            mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent,
+                    options, mCallbacks, false /* useSyntheticRecentsTransition */);
+        }
+
+        if (enableHandleDelayedGestureCallbacks()) {
+            ActiveGestureProtoLogProxy.logSettingRecentsAnimationStartPending(
+                    mRecentsAnimationStartPending);
         }
         gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED);
         return mCallbacks;
@@ -373,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);
@@ -455,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 */);
@@ -493,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 80902e3..0dbdcb7 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -116,6 +116,7 @@
             TaskShortcutFactory.INSTALL,
             TaskShortcutFactory.FREE_FORM,
             DesktopSystemShortcut.Companion.createFactory(),
+            ExternalDisplaySystemShortcut.Companion.createFactory(),
             TaskShortcutFactory.WELLBEING,
             TaskShortcutFactory.SAVE_APP_PAIR,
             TaskShortcutFactory.SCREENSHOT,
@@ -173,9 +174,8 @@
 
         protected T getActionsView() {
             if (mActionsView == null) {
-                mActionsView = BaseActivity.fromContext(
-                        mTaskContainer.getTaskView().getContext()).findViewById(
-                        R.id.overview_actions_view);
+                mActionsView = (T) RecentsViewContainer.containerFromContext(
+                        mTaskContainer.getTaskView().getContext()).getActionsView();
             }
             return mActionsView;
         }
@@ -184,6 +184,10 @@
             return mTaskContainer.getTaskView();
         }
 
+        public View getSnapshotView() {
+            return mTaskContainer.getSnapshotView();
+        }
+
         /**
          * Called when the current task is interactive for the user
          */
@@ -312,7 +316,7 @@
             // the difference between the bitmap bounds and the projected view bounds.
             Matrix boundsToBitmapSpace = new Matrix();
             Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
-                    ? mHelper.getEnabledState().getThumbnailMatrix()
+                    ? mHelper.getThumbnailMatrix()
                     : mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
             thumbnailMatrix.invert(boundsToBitmapSpace);
             RectF boundsInBitmapSpace = new RectF();
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 77124bf..785666f 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -23,7 +23,6 @@
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
 
 import android.app.ActivityOptions;
 import android.graphics.Bitmap;
@@ -63,6 +62,7 @@
 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
 import com.android.systemui.shared.recents.view.RecentsTransition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.util.Collections;
 import java.util.List;
@@ -315,12 +315,12 @@
             boolean isTaskSplitNotSupported = !task.isDockable ||
                     (intentFlags & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
             boolean hideForExistingMultiWindow = container.getDeviceProfile().isMultiWindowMode;
-            boolean isFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
+            boolean isLargeTile = deviceProfile.isTablet && taskView.isLargeTile();
             boolean isTaskInExpectedScrollPosition =
                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
 
             if (notEnoughTasksToSplit || isTaskSplitNotSupported || hideForExistingMultiWindow
-                    || (isFocusedTask && isTaskInExpectedScrollPosition)) {
+                    || (isLargeTile && isTaskInExpectedScrollPosition)) {
                 return null;
             }
 
@@ -340,11 +340,11 @@
             DeviceProfile deviceProfile = container.getDeviceProfile();
             final TaskView taskView = taskContainer.getTaskView();
             final RecentsView recentsView = taskView.getRecentsView();
-            boolean isLargeTileFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
+            boolean isLargeTile = deviceProfile.isTablet && taskView.isLargeTile();
             boolean isInExpectedScrollPosition =
                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
             boolean shouldShowActionsButtonInstead =
-                    isLargeTileFocusedTask && isInExpectedScrollPosition;
+                    isLargeTile && isInExpectedScrollPosition;
 
             // No "save app pair" menu item if:
             // - we are in 3p launcher
@@ -394,7 +394,7 @@
             return Settings.Global.getInt(
                     container.asContext().getContentResolver(),
                     Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0
-                    && !enableDesktopWindowingMode();
+                    && !DesktopModeStatus.canEnterDesktopMode(container.asContext());
         }
     };
 
@@ -431,7 +431,7 @@
 
         @Override
         public void onClick(View view) {
-            if (mTaskView.launchTaskAnimated() != null) {
+            if (mTaskView.launchAsStaticTile() != null) {
                 SystemUiProxy.INSTANCE.get(mTarget.asContext()).startScreenPinning(
                         mTaskView.getFirstTask().key.id);
             }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 3c6c3e4..580dcc2 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.recents.data.HighResLoadingStateNotifier;
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
 import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
 import com.android.quickstep.util.TaskKeyCache;
@@ -48,7 +49,7 @@
     private final boolean mEnableTaskSnapshotPreloading;
     private final Context mContext;
 
-    public static class HighResLoadingState {
+    public static class HighResLoadingState implements HighResLoadingStateNotifier {
         private boolean mForceHighResThumbnails;
         private boolean mVisible;
         private boolean mFlingingFast;
@@ -65,11 +66,13 @@
             mForceHighResThumbnails = !supportsLowResThumbnails();
         }
 
-        public void addCallback(HighResLoadingStateChangedCallback callback) {
+        @Override
+        public void addCallback(@NonNull HighResLoadingStateChangedCallback callback) {
             mCallbacks.add(callback);
         }
 
-        public void removeCallback(HighResLoadingStateChangedCallback callback) {
+        @Override
+        public void removeCallback(@NonNull HighResLoadingStateChangedCallback callback) {
             mCallbacks.remove(callback);
         }
 
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 bd44283..07ee479 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -59,7 +59,6 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.jank.Cuj;
-import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimationSuccessListener;
@@ -68,6 +67,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
@@ -80,6 +80,7 @@
 import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.animation.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.recents.model.Task;
@@ -155,8 +156,9 @@
         return taskView;
     }
 
-    public static void createRecentsWindowAnimator(
-            @NonNull RecentsView recentsView,
+    public static <T extends Context & RecentsViewContainer & StatefulContainer<?>>
+    void createRecentsWindowAnimator(
+            @NonNull RecentsView<T, ?> recentsView,
             @NonNull TaskView v,
             boolean skipViewChanges,
             @NonNull RemoteAnimationTarget[] appTargets,
@@ -204,8 +206,9 @@
 
         int taskIndex = recentsView.indexOfChild(v);
         Context context = v.getContext();
-        BaseActivity baseActivity = BaseActivity.fromContext(context);
-        DeviceProfile dp = baseActivity.getDeviceProfile();
+
+        T container = RecentsViewContainer.containerFromContext(context);
+        DeviceProfile dp = container.getDeviceProfile();
         boolean showAsGrid = dp.isTablet;
         boolean parallaxCenterAndAdjacentTask =
                 !showAsGrid && taskIndex != recentsView.getCurrentPage();
@@ -232,9 +235,13 @@
                         TaskViewSimulator::setTaskRectTranslation, taskRectTranslationPrimary,
                         taskRectTranslationSecondary);
 
-                // Fade in the task during the initial 20% of the animation
-                out.addFloat(targetHandle.getTransformParams(), TransformParams.TARGET_ALPHA, 0, 1,
-                        clampToProgress(LINEAR, 0, 0.2f));
+                if (v instanceof DesktopTaskView) {
+                    targetHandle.getTransformParams().setTargetAlpha(1f);
+                } else {
+                    // Fade in the task during the initial 20% of the animation
+                    out.addFloat(targetHandle.getTransformParams(), TransformParams.TARGET_ALPHA, 0,
+                            1, clampToProgress(LINEAR, 0, 0.2f));
+                }
             }
         }
 
@@ -300,14 +307,6 @@
                         }
                     }
                 });
-            } else {
-                // There is no transition animation for app launch from recent in live tile mode so
-                // we have to trigger the navigation bar animation from system here.
-                final RecentsAnimationController controller =
-                        recentsView.getRecentsAnimationController();
-                if (controller != null) {
-                    controller.animateNavigationBarToApp(RECENTS_LAUNCH_DURATION);
-                }
             }
             topMostSimulators = remoteTargetHandles;
         }
@@ -421,7 +420,8 @@
 
         if (depthController != null) {
             out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
-                    BACKGROUND_APP.getDepth(baseActivity), TOUCH_RESPONSE);
+                    BACKGROUND_APP.getDepth(container),
+                    TOUCH_RESPONSE);
         }
     }
 
@@ -558,7 +558,7 @@
     /**
      * Start recents to desktop animation
      */
-    public static void composeRecentsDesktopLaunchAnimator(
+    public static AnimatorSet composeRecentsDesktopLaunchAnimator(
             @NonNull DesktopTaskView launchingTaskView,
             @NonNull StateManager stateManager, @Nullable DepthController depthController,
             @NonNull TransitionInfo transitionInfo,
@@ -588,7 +588,7 @@
                 true /* launcherClosing */, stateManager, launchingTaskView.getRecentsView(),
                 depthController);
 
-        animatorSet.start();
+        return animatorSet;
     }
 
     public static void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 6ed05c8..c9dfe6d 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -27,8 +27,8 @@
 
 import android.annotation.UserIdInt;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
 import android.content.Context;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -48,7 +48,6 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
 
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -63,17 +62,13 @@
 public class TopTaskTracker extends ISplitScreenListener.Stub
         implements TaskStackChangeListener, SafeCloseable {
 
-    private static final String TAG = "TopTaskTracker";
-
-    private static final boolean DEBUG = true;
-
     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 LinkedList<TaskInfo> mOrderedTaskList = new LinkedList<>();
 
     private final Context mContext;
     private final SplitStageInfo mMainStagePosition = new SplitStageInfo();
@@ -98,19 +93,14 @@
     @Override
     public void onTaskRemoved(int taskId) {
         mOrderedTaskList.removeIf(rto -> rto.taskId == taskId);
-        if (DEBUG) {
-            Log.i(TAG, "onTaskRemoved: taskId=" + taskId);
-        }
     }
 
     @Override
     public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
-        if (!mOrderedTaskList.isEmpty()
-                && mOrderedTaskList.getFirst().taskId != taskInfo.taskId
-                && DEBUG) {
-            Log.i(TAG, "onTaskMovedToFront: (moved taskInfo to front) taskId=" + taskInfo.taskId
-                    + ", baseIntent=" + taskInfo.baseIntent);
-        }
+        handleTaskMovedToFront(taskInfo);
+    }
+
+    public void handleTaskMovedToFront(TaskInfo taskInfo) {
         mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId);
         mOrderedTaskList.addFirst(taskInfo);
 
@@ -118,14 +108,9 @@
         // 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) {
-                if (DEBUG) {
-                    Log.i(TAG, "onTaskMovedToFront: (removing top task on home display) taskId="
-                            + topTaskOnHomeDisplay.taskId
-                            + ", baseIntent=" + topTaskOnHomeDisplay.baseIntent);
-                }
                 mOrderedTaskList.removeIf(rto -> rto.taskId == topTaskOnHomeDisplay.taskId);
                 mOrderedTaskList.addFirst(topTaskOnHomeDisplay);
             }
@@ -133,16 +118,12 @@
 
         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) {
-                    if (DEBUG) {
-                        Log.i(TAG, "onTaskMovedToFront: (removing task list overflow) taskId="
-                                + taskInfo.taskId + ", baseIntent=" + taskInfo.baseIntent);
-                    }
                     itr.remove();
                     return;
                 }
@@ -152,9 +133,6 @@
 
     @Override
     public void onStagePositionChanged(@StageType int stage, @StagePosition int position) {
-        if (DEBUG) {
-            Log.i(TAG, "onStagePositionChanged: stage=" + stage + ", position=" + position);
-        }
         if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
             mMainStagePosition.stagePosition = position;
         } else {
@@ -162,12 +140,17 @@
         }
     }
 
+    public void onTaskChanged(RunningTaskInfo taskInfo) {
+        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 (DEBUG) {
-            Log.i(TAG, "onTaskStageChanged: taskId=" + taskId
-                    + ", stage=" + stage + ", visible=" + visible);
-        }
         // 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) {
@@ -187,18 +170,11 @@
 
     @Override
     public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
-        if (DEBUG) {
-            Log.i(TAG, "onActivityPinned: packageName=" + packageName
-                    + ", userId=" + userId + ", stackId=" + stackId);
-        }
         mPinnedTaskId = taskId;
     }
 
     @Override
     public void onActivityUnpinned() {
-        if (DEBUG) {
-            Log.i(TAG, "onActivityUnpinned");
-        }
         mPinnedTaskId = INVALID_TASK_ID;
     }
 
@@ -244,25 +220,15 @@
             Collections.addAll(mOrderedTaskList, tasks);
         }
 
-        // Strip the pinned task
-        ArrayList<RunningTaskInfo> tasks = new ArrayList<>(mOrderedTaskList);
-        tasks.removeIf(t -> t.taskId == mPinnedTaskId);
+        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);
     }
 
-    public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "TopTaskTracker:");
-
-        writer.println(prefix + "\tmOrderedTaskList=[");
-        for (RunningTaskInfo taskInfo : mOrderedTaskList) {
-            writer.println(prefix + "\t\t(taskId=" + taskInfo.taskId
-                    + "; baseIntent=" + taskInfo.baseIntent
-                    + "; isRunning=" + taskInfo.isRunning + ")");
-        }
-        writer.println(prefix + "\t]");
-        writer.println(prefix + "\tmMainStagePosition=" + mMainStagePosition);
-        writer.println(prefix + "\tmSideStagePosition=" + mSideStagePosition);
-        writer.println(prefix + "\tmPinnedTaskId=" + mPinnedTaskId);
+    private static boolean isRecentsTask(TaskInfo task) {
+        return task != null && task.configuration.windowConfiguration
+                .getActivityType() == ACTIVITY_TYPE_RECENTS;
     }
 
     /**
@@ -272,10 +238,10 @@
     public static class CachedTaskInfo {
 
         @Nullable
-        private final RunningTaskInfo mTopTask;
-        public final List<RunningTaskInfo> mAllCachedTasks;
+        private final TaskInfo mTopTask;
+        public final List<TaskInfo> mAllCachedTasks;
 
-        CachedTaskInfo(List<RunningTaskInfo> allCachedTasks) {
+        CachedTaskInfo(List<TaskInfo> allCachedTasks) {
             mAllCachedTasks = allCachedTasks;
             mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0);
         }
@@ -301,7 +267,7 @@
                 // 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
@@ -320,8 +286,7 @@
         }
 
         public boolean isRecentsTask() {
-            return mTopTask != null && mTopTask.configuration.windowConfiguration
-                    .getActivityType() == ACTIVITY_TYPE_RECENTS;
+            return TopTaskTracker.isRecentsTask(mTopTask);
         }
 
         /**
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2896979..ad5720f 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep;
 
-import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_MOVE;
@@ -26,11 +25,9 @@
 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.Launcher.INTENT_ACTION_ALL_APPS_TOGGLE;
 import static com.android.launcher3.LauncherPrefs.backedUpItem;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
 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.OnboardingPrefs.HOME_BOUNCE_SEEN;
@@ -39,26 +36,21 @@
 import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
 import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
 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.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
+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;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
 
 import android.app.PendingIntent;
 import android.app.Service;
@@ -79,24 +71,28 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.MotionEvent;
+import android.view.View;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ConstantItem;
 import com.android.launcher3.EncryptionType;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.desktop.DesktopAppLaunchTransitionManager;
 import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarManager;
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -107,8 +103,12 @@
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.OverviewCommandHelper.CommandType;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
+import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
 import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer;
 import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
@@ -123,8 +123,9 @@
 import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
-import com.android.quickstep.util.AssistStateManager;
-import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
+import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -223,7 +224,6 @@
         @BinderThread
         @Override
         public void onTaskbarToggled() {
-            if (!FeatureFlags.ENABLE_KEYBOARD_TASKBAR_TOGGLE.get()) return;
             MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
                 TaskbarActivityContext activityContext =
                         tis.mTaskbarManager.getCurrentActivityContext();
@@ -243,7 +243,7 @@
                     return;
                 }
                 TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-                tis.mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_TOGGLE);
+                tis.mOverviewCommandHelper.addCommand(CommandType.TOGGLE);
             });
         }
 
@@ -253,10 +253,9 @@
             executeForTouchInteractionService(tis -> {
                 if (triggeredFromAltTab) {
                     TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-                    tis.mOverviewCommandHelper.addCommand(
-                            OverviewCommandHelper.TYPE_KEYBOARD_INPUT);
+                    tis.mOverviewCommandHelper.addCommand(CommandType.KEYBOARD_INPUT);
                 } else {
-                    tis.mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW);
+                    tis.mOverviewCommandHelper.addCommand(CommandType.SHOW);
                 }
             });
         }
@@ -267,7 +266,7 @@
             executeForTouchInteractionService(tis -> {
                 if (triggeredFromAltTab && !triggeredFromHomeKey) {
                     // onOverviewShownFromAltTab hides the overview and ends at the target app
-                    tis.mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HIDE);
+                    tis.mOverviewCommandHelper.addCommand(CommandType.HIDE);
                 }
             });
         }
@@ -300,7 +299,8 @@
         @Override
         public void onAssistantOverrideInvoked(int invocationType) {
             executeForTouchInteractionService(tis -> {
-                if (!AssistUtils.newInstance(tis).tryStartAssistOverride(invocationType)) {
+                if (!ContextualSearchInvoker.newInstance(tis)
+                        .tryStartAssistOverride(invocationType)) {
                     Log.w(TAG, "Failed to invoke Assist override");
                 }
             });
@@ -325,10 +325,10 @@
         @Override
         public void enterStageSplitFromRunningApp(boolean leftOrTop) {
             executeForTouchInteractionService(tis -> {
-                StatefulActivity activity =
-                        tis.mOverviewComponentObserver.getActivityInterface().getCreatedContainer();
-                if (activity != null) {
-                    activity.enterStageSplitFromRunningApp(leftOrTop);
+                RecentsViewContainer container = tis.mOverviewComponentObserver
+                        .getContainerInterface().getCreatedContainer();
+                if (container != null) {
+                    container.enterStageSplitFromRunningApp(leftOrTop);
                 }
             });
         }
@@ -344,35 +344,44 @@
 
         @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
+        @Override
+        public void appTransitionPending(boolean pending) {
+            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
+                    executeForTaskbarManager(
+                            taskbarManager -> taskbarManager.appTransitionPending(pending))
             ));
         }
 
@@ -450,6 +459,18 @@
             return tis.mTaskbarManager;
         }
 
+        /**
+         * Returns the {@link DesktopVisibilityController}
+         * <p>
+         * Returns {@code null} if TouchInteractionService is not connected
+         */
+        @Nullable
+        public DesktopVisibilityController getDesktopVisibilityController() {
+            TouchInteractionService tis = mTis.get();
+            if (tis == null) return null;
+            return tis.mDesktopVisibilityController;
+        }
+
         @VisibleForTesting
         public void injectFakeTrackpadForTesting() {
             TouchInteractionService tis = mTis.get();
@@ -531,11 +552,15 @@
                 @Override
                 public void onInputDeviceAdded(int deviceId) {
                     if (isTrackpadDevice(deviceId)) {
-                        boolean wasEmpty = mTrackpadsConnected.isEmpty();
-                        mTrackpadsConnected.add(deviceId);
-                        if (wasEmpty) {
-                            update();
-                        }
+                        // This updates internal TIS state so it needs to also run on the main
+                        // thread.
+                        MAIN_EXECUTOR.execute(() -> {
+                            boolean wasEmpty = mTrackpadsConnected.isEmpty();
+                            mTrackpadsConnected.add(deviceId);
+                            if (wasEmpty) {
+                                update();
+                            }
+                        });
                     }
                 }
 
@@ -545,12 +570,17 @@
 
                 @Override
                 public void onInputDeviceRemoved(int deviceId) {
-                    mTrackpadsConnected.remove(deviceId);
-                    if (mTrackpadsConnected.isEmpty()) {
-                        update();
-                    }
+                    // This updates internal TIS state so it needs to also run on the main
+                    // thread.
+                    MAIN_EXECUTOR.execute(() -> {
+                        mTrackpadsConnected.remove(deviceId);
+                        if (mTrackpadsConnected.isEmpty()) {
+                            update();
+                        }
+                    });
                 }
 
+                @MainThread
                 private void update() {
                     if (mInputMonitorCompat != null && !mTrackpadsConnected.isEmpty()) {
                         // Don't destroy and reinitialize input monitor due to trackpad
@@ -561,6 +591,7 @@
                 }
 
                 private boolean isTrackpadDevice(int deviceId) {
+                    // This is a blocking binder call that should run on a bg thread.
                     InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
                     if (inputDevice == null) {
                         return false;
@@ -586,18 +617,23 @@
             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;
 
     private final TaskbarNavButtonCallbacks mNavCallbacks = new TaskbarNavButtonCallbacks() {
         @Override
         public void onNavigateHome() {
-            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME);
+            mOverviewCommandHelper.addCommand(CommandType.HOME);
         }
 
         @Override
         public void onToggleOverview() {
-            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_TOGGLE);
+            mOverviewCommandHelper.addCommand(CommandType.TOGGLE);
         }
     };
 
@@ -618,6 +654,7 @@
     private InputEventReceiver mInputEventReceiver;
 
     private TaskbarManager mTaskbarManager;
+    private RecentsWindowManager mRecentsWindowManager;
     private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
     private AllAppsActionManager mAllAppsActionManager;
     private InputManager mInputManager;
@@ -625,9 +662,14 @@
 
     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();
@@ -637,20 +679,25 @@
         mAllAppsActionManager = new AllAppsActionManager(
                 this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent);
         mInputManager = getSystemService(InputManager.class);
-        if (ENABLE_TRACKPAD_GESTURE.get()) {
-            mInputManager.registerInputDeviceListener(mInputDeviceListener,
-                    UI_HELPER_EXECUTOR.getHandler());
-            int [] inputDevices = mInputManager.getInputDeviceIds();
-            for (int inputDeviceId : inputDevices) {
-                mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
-            }
+        mInputManager.registerInputDeviceListener(mInputDeviceListener,
+                UI_HELPER_EXECUTOR.getHandler());
+        int [] inputDevices = mInputManager.getInputDeviceIds();
+        for (int inputDeviceId : inputDevices) {
+            mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
         }
-        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks);
+        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;
 
@@ -658,7 +705,8 @@
     }
 
     private void disposeEventHandlers(String reason) {
-        Log.d(TAG, "disposeEventHandlers: Reason: " + reason);
+        Log.d(TAG, "disposeEventHandlers: Reason: " + reason
+                + " instance=" + System.identityHashCode(this));
         if (mInputEventReceiver != null) {
             mInputEventReceiver.dispose();
             mInputEventReceiver = null;
@@ -672,8 +720,9 @@
     private void initInputMonitor(String reason) {
         disposeEventHandlers("Initializing input monitor due to: " + reason);
 
-        if (mDeviceState.isButtonNavMode() && (!ENABLE_TRACKPAD_GESTURE.get()
-                || mTrackpadsConnected.isEmpty())) {
+        if (mDeviceState.isButtonNavMode()
+                && !mDeviceState.supportsAssistantGestureInButtonNav()
+                && (mTrackpadsConnected.isEmpty())) {
             return;
         }
 
@@ -694,8 +743,9 @@
 
     @UiThread
     public void onUserUnlocked() {
-        Log.d(TAG, "onUserUnlocked: userId=" + getUserId());
-        mTaskAnimationManager = new TaskAnimationManager(this);
+        Log.d(TAG, "onUserUnlocked: userId=" + getUserId()
+                + " instance=" + System.identityHashCode(this));
+        mTaskAnimationManager = new TaskAnimationManager(this, mRecentsWindowManager);
         mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
         mOverviewCommandHelper = new OverviewCommandHelper(this,
                 mOverviewComponentObserver, mTaskAnimationManager);
@@ -714,6 +764,8 @@
 
         mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
         onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame());
+
+        mTaskbarManager.onUserUnlocked();
     }
 
     public OverviewCommandHelper getOverviewCommandHelper() {
@@ -738,33 +790,28 @@
 
     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();
     }
 
     private PendingIntent createAllAppsPendingIntent() {
-        if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
-            return new PendingIntent(new IIntentSender.Stub() {
-                @Override
-                public void send(int code, Intent intent, String resolvedType,
-                        IBinder allowlistToken, IIntentReceiver finishedReceiver,
-                        String requiredPermission, Bundle options) {
-                    MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllApps());
-                }
-            });
-        } else {
-            return PendingIntent.getActivity(
-                    this,
-                    GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
-                    new Intent(mOverviewComponentObserver.getHomeIntent())
-                            .setAction(INTENT_ACTION_ALL_APPS_TOGGLE),
-                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-        }
+        return new PendingIntent(new IIntentSender.Stub() {
+            @Override
+            public void send(int code, Intent intent, String resolvedType,
+                    IBinder allowlistToken, IIntentReceiver finishedReceiver,
+                    String requiredPermission, Bundle options) {
+                MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllApps());
+            }
+        });
     }
 
     @UiThread
@@ -781,14 +828,15 @@
     @UiThread
     private void onAssistantVisibilityChanged() {
         if (LockedUserState.get(this).isUserUnlocked()) {
-            mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged(
+            mOverviewComponentObserver.getContainerInterface().onAssistantVisibilityChanged(
                     mDeviceState.getAssistantVisibility());
         }
     }
 
     @Override
     public void onDestroy() {
-        Log.d(TAG, "Touch service destroyed: user=" + getUserId());
+        Log.d(TAG, "onDestroy: user=" + getUserId()
+                + " instance=" + System.identityHashCode(this));
         sIsInitialized = false;
         if (LockedUserState.get(this).isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
@@ -804,15 +852,26 @@
         mTrackpadsConnected.clear();
 
         mTaskbarManager.destroy();
+
+        if (mRecentsWindowManager != null) {
+            mRecentsWindowManager.destroy();
+        }
+        if (mDesktopAppLaunchTransitionManager != null) {
+            mDesktopAppLaunchTransitionManager.unregisterTransitions();
+        }
+        mDesktopAppLaunchTransitionManager = null;
+        mDesktopVisibilityController.onDestroy();
         sConnected = false;
 
+        LockedUserState.get(this).removeOnUserUnlockedRunnable(mUserUnlockedRunnable);
         ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener);
         super.onDestroy();
     }
 
     @Override
     public IBinder onBind(Intent intent) {
-        Log.d(TAG, "Touch service connected: user=" + getUserId());
+        Log.d(TAG, "onBind: user=" + getUserId()
+                + " instance=" + System.identityHashCode(this));
         return mTISBinder;
     }
 
@@ -829,9 +888,7 @@
 
     private void onInputEvent(InputEvent ev) {
         if (!(ev instanceof MotionEvent)) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event: received unknown event ")
-                    .append(ev.toString()));
+            ActiveGestureProtoLogProxy.logUnknownInputEvent(ev.toString());
             return;
         }
         MotionEvent event = (MotionEvent) ev;
@@ -840,25 +897,19 @@
                 TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
 
         if (!LockedUserState.get(this).isUserUnlocked()) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event: user is locked"));
+            ActiveGestureProtoLogProxy.logOnInputEventUserLocked();
             return;
         }
 
         NavigationMode currentNavMode = mDeviceState.getMode();
         if (mGestureStartNavMode != null && mGestureStartNavMode != currentNavMode) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                            .append("Navigation mode switched mid-gesture (")
-                            .append(mGestureStartNavMode.name())
-                            .append(" -> ")
-                            .append(currentNavMode.name())
-                            .append("); cancelling gesture."),
-                    NAVIGATION_MODE_SWITCHED);
+            ActiveGestureProtoLogProxy.logOnInputEventNavModeSwitched(
+                    mGestureStartNavMode.name(), currentNavMode.name());
             event.setAction(ACTION_CANCEL);
-        } else if (mDeviceState.isButtonNavMode() && !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"));
+        } else if (mDeviceState.isButtonNavMode()
+                && !mDeviceState.supportsAssistantGestureInButtonNav()
+                && !isTrackpadMotionEvent(event)) {
+            ActiveGestureProtoLogProxy.logOnInputEventThreeButtonNav();
             return;
         }
 
@@ -874,12 +925,7 @@
             }
             if (mTaskAnimationManager.shouldIgnoreMotionEvents()) {
                 if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("TIS.onMotionEvent: A new gesture has been ")
-                                    .append("started, but a previously-requested recents ")
-                                    .append("animation hasn't started. Ignoring all following ")
-                                    .append("motion events."),
-                            RECENTS_ANIMATION_START_PENDING);
+                    ActiveGestureProtoLogProxy.logOnInputIgnoringFollowingEvents();
                 }
                 return;
             }
@@ -894,22 +940,37 @@
         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);
 
             boolean isOneHandedModeActive = mDeviceState.isOneHandedModeActive();
             boolean isInSwipeUpTouchRegion = mRotationTouchHelper.isInSwipeUpTouchRegion(event);
             TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
-            if (isInSwipeUpTouchRegion && tac != null) {
-                tac.closeKeyboardQuickSwitchView();
-            }
-            if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
-                    || isHoverActionWithoutConsumer) {
+            BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null;
+            boolean isOnBubbles = bubbleControllers != null
+                    && BubbleBarInputConsumer.isEventOnBubbles(tac, event);
+            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, "
+                            + "consuming gesture for assistant action");
+                    mGestureState =
+                            createGestureState(mGestureState, getTrackpadGestureType(event));
+                    mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
+                } else {
+                    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);
@@ -922,19 +983,18 @@
             } else if ((mDeviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
                     && mDeviceState.canTriggerAssistantAction(event)) {
                 reasonString.append(mDeviceState.isFullyGesturalNavMode()
-                                ? "using fully gestural nav"
-                                : "event is a trackpad multi-finger swipe")
-                        .append(" and event can trigger assistant action")
-                        .append(", consuming gesture for assistant action");
-                mGestureState = createGestureState(mGestureState,
-                        getTrackpadGestureType(event));
+                        ? "using fully gestural nav and event can trigger assistant action, "
+                                + "consuming gesture for assistant action"
+                        : "event is a trackpad multi-finger swipe and event can trigger assistant "
+                                + "action, consuming gesture for assistant action");
+                mGestureState = createGestureState(mGestureState, getTrackpadGestureType(event));
                 // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
                 // should not interrupt it. QuickSwitch assumes that interruption can only
                 // happen if the next gesture is also quick switch.
                 mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
             } else if (mDeviceState.canTriggerOneHandedAction(event)) {
-                reasonString.append("event can trigger one-handed action")
-                                .append(", consuming gesture for one-handed action");
+                reasonString.append("event can trigger one-handed action, "
+                        + "consuming gesture for one-handed action");
                 // Consume gesture event for triggering one handed feature.
                 mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
                         InputConsumer.NO_OP, mInputMonitorCompat);
@@ -952,41 +1012,25 @@
         if (mUncheckedConsumer != InputConsumer.NO_OP) {
             switch (action) {
                 case ACTION_DOWN:
-                    ActiveGestureLog.INSTANCE.addLog(reasonString);
+                    ActiveGestureProtoLogProxy.logOnInputEventActionDown(reasonString);
                     // fall through
                 case ACTION_UP:
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("onMotionEvent(")
-                                    .append((int) event.getRawX())
-                                    .append(", ")
-                                    .append((int) event.getRawY())
-                                    .append("): ")
-                                    .append(MotionEvent.actionToString(action))
-                                    .append(", ")
-                                    .append(MotionEvent.classificationToString(
-                                            event.getClassification())),
-                            /* gestureEvent= */ action == ACTION_DOWN
-                                    ? MOTION_DOWN
-                                    : MOTION_UP);
+                    ActiveGestureProtoLogProxy.logOnInputEventActionUp(
+                            (int) event.getRawX(),
+                            (int) event.getRawY(),
+                            action,
+                            MotionEvent.classificationToString(event.getClassification()));
                     break;
                 case ACTION_MOVE:
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("onMotionEvent: ")
-                                    .append(MotionEvent.actionToString(action))
-                                    .append(",")
-                                    .append(MotionEvent.classificationToString(
-                                            event.getClassification()))
-                                    .append(", pointerCount: ")
-                                    .append(event.getPointerCount()),
-                            MOTION_MOVE);
+                    ActiveGestureProtoLogProxy.logOnInputEventActionMove(
+                            MotionEvent.actionToString(action),
+                            MotionEvent.classificationToString(event.getClassification()),
+                            event.getPointerCount());
                     break;
                 default: {
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("onMotionEvent: ")
-                                    .append(MotionEvent.actionToString(action))
-                                    .append(",")
-                                    .append(MotionEvent.classificationToString(
-                                            event.getClassification())));
+                    ActiveGestureProtoLogProxy.logOnInputEventGenericAction(
+                            MotionEvent.actionToString(action),
+                            MotionEvent.classificationToString(event.getClassification()));
                 }
             }
         }
@@ -1041,11 +1085,11 @@
             MotionEvent motionEvent,
             CompoundString reasonString) {
         if (mDeviceState.isGestureBlockedTask(gestureState.getRunningTask())) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("is gesture-blocked task, using base input consumer");
+            reasonString.append(
+                    "%sis gesture-blocked task, using base input consumer", SUBSTRING_PREFIX);
             return base;
         } else {
-            reasonString.append(SUBSTRING_PREFIX).append("using AssistantInputConsumer");
+            reasonString.append("%susing AssistantInputConsumer", SUBSTRING_PREFIX);
             return new AssistantInputConsumer(
                     this, gestureState, base, mInputMonitorCompat, mDeviceState, motionEvent);
         }
@@ -1076,15 +1120,22 @@
         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(
@@ -1107,12 +1158,11 @@
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
                 consumer = createDeviceLockedInputConsumer(
-                        newGestureState, reasonString.append(SUBSTRING_PREFIX)
-                                .append("can start system gesture"));
+                        newGestureState,
+                        reasonString.append("%scan start system gesture", SUBSTRING_PREFIX));
             } else {
                 consumer = getDefaultInputConsumer(
-                        reasonString.append(SUBSTRING_PREFIX)
-                                .append("cannot start system gesture"));
+                        reasonString.append("%scannot start system gesture", SUBSTRING_PREFIX));
             }
             logInputConsumerSelectionReason(consumer, reasonString);
             return consumer;
@@ -1124,13 +1174,12 @@
         // a followup gesture and the first gesture started in a valid system state.
         if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning()) {
             reasonString = newCompoundString(canStartSystemGesture
-                    ? "can start system gesture" : "recents animation was running")
-                    .append(", trying to use base consumer");
+                    ? "can start system gesture, trying to use base consumer"
+                    : "recents animation was running, trying to use base consumer");
             base = newBaseConsumer(previousGestureState, newGestureState, event, reasonString);
         } else {
-            reasonString = newCompoundString(
-                    "cannot start system gesture and recents animation was not running")
-                    .append(", trying to use default input consumer");
+            reasonString = newCompoundString("cannot start system gesture and recents "
+                    + "animation was not running, trying to use default input consumer");
             base = getDefaultInputConsumer(reasonString);
         }
         if (mDeviceState.isGesturalNavMode() || newGestureState.isTrackpadGesture()) {
@@ -1140,27 +1189,26 @@
             String reasonPrefix =
                     "device is in gesture navigation mode or 3-button mode with a trackpad gesture";
             if (mDeviceState.canTriggerAssistantAction(event)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("gesture can trigger the assistant")
-                        .append(", trying to use assistant input consumer");
+                reasonString.append("%s%s%sgesture can trigger the assistant, "
+                                + "trying to use assistant input consumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString);
             }
 
             // If Taskbar is present, we listen for swipe or cursor hover events to unstash it.
-            TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
             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");
+                    reasonString.append("%s%s%sTaskbarActivityContext != null, "
+                                    + "using TaskbarUnstashInputConsumer",
+                            NEWLINE_PREFIX,
+                            reasonPrefix,
+                            SUBSTRING_PREFIX);
                     base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
                             mOverviewCommandHelper, mGestureState);
                 }
@@ -1169,9 +1217,9 @@
                 // Create bubbles input consumer before NavHandleLongPressInputConsumer.
                 // This allows for nav handle to fall back to bubbles.
                 if (mDeviceState.isBubblesExpanded()) {
-                    reasonString = newCompoundString(reasonPrefix)
-                            .append(SUBSTRING_PREFIX)
-                            .append("bubbles expanded, trying to use default input consumer");
+                    reasonString = newCompoundString(reasonPrefix).append(
+                            "%sbubbles expanded, trying to use default input consumer",
+                            SUBSTRING_PREFIX);
                     // Bubbles can handle home gesture itself.
                     base = getDefaultInputConsumer(reasonString);
                 }
@@ -1180,11 +1228,12 @@
             NavHandle navHandle = tac != null ? tac.getNavHandle()
                     : SystemUiProxy.INSTANCE.get(this);
             if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
-                    && navHandle.canNavHandleBeLongPressed()) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("Not running recents animation, ");
+                    && navHandle.canNavHandleBeLongPressed()
+                    && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
+                reasonString.append("%s%s%sNot running recents animation, ",
+                                NEWLINE_PREFIX,
+                                reasonPrefix,
+                                SUBSTRING_PREFIX);
                 if (tac != null && tac.getNavHandle().canNavHandleBeLongPressed()) {
                     reasonString.append("stashed handle is long-pressable, ");
                 }
@@ -1196,74 +1245,74 @@
             if (!enableBubblesLongPressNavHandle()) {
                 // Continue overriding nav handle input consumer with bubbles
                 if (mDeviceState.isBubblesExpanded()) {
-                    reasonString = newCompoundString(reasonPrefix)
-                            .append(SUBSTRING_PREFIX)
-                            .append("bubbles expanded, trying to use default input consumer");
+                    reasonString = newCompoundString(reasonPrefix).append(
+                            "%sbubbles expanded, trying to use default input consumer",
+                            SUBSTRING_PREFIX);
                     // Bubbles can handle home gesture itself.
                     base = getDefaultInputConsumer(reasonString);
                 }
             }
 
             if (mDeviceState.isSystemUiDialogShowing()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("system dialog is showing, using SysUiOverlayInputConsumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%ssystem dialog is showing, using SysUiOverlayInputConsumer",
+                        SUBSTRING_PREFIX);
                 base = new SysUiOverlayInputConsumer(
                         getBaseContext(), mDeviceState, mInputMonitorCompat);
             }
 
-            if (ENABLE_TRACKPAD_GESTURE.get() && mGestureState.isTrackpadGesture()
+            if (mGestureState.isTrackpadGesture()
                     && canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("Trackpad 3-finger gesture, using TrackpadStatusBarInputConsumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%sTrackpad 3-finger gesture, using TrackpadStatusBarInputConsumer",
+                        SUBSTRING_PREFIX);
                 base = new TrackpadStatusBarInputConsumer(getBaseContext(), base,
                         mInputMonitorCompat);
             }
 
             if (mDeviceState.isScreenPinningActive()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("screen pinning is active, using ScreenPinnedInputConsumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%sscreen pinning is active, using ScreenPinnedInputConsumer",
+                        SUBSTRING_PREFIX);
                 // Note: we only allow accessibility to wrap this, and it replaces the previous
                 // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
                 base = new ScreenPinnedInputConsumer(this, newGestureState);
             }
 
             if (mDeviceState.canTriggerOneHandedAction(event)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("gesture can trigger one handed mode")
-                        .append(", using OneHandedModeInputConsumer");
+                reasonString.append("%s%s%sgesture can trigger one handed mode, "
+                                + "using OneHandedModeInputConsumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = new OneHandedModeInputConsumer(
                         this, mDeviceState, base, mInputMonitorCompat);
             }
 
             if (mDeviceState.isAccessibilityMenuAvailable()) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("accessibility menu is available")
-                        .append(", using AccessibilityInputConsumer");
+                reasonString.append(
+                        "%s%s%saccessibility menu is available, using AccessibilityInputConsumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = new AccessibilityInputConsumer(
                         this, mDeviceState, mGestureState, base, mInputMonitorCompat);
             }
         } else {
             String reasonPrefix = "device is not in gesture navigation mode";
             if (mDeviceState.isScreenPinningActive()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("screen pinning is active, trying to use default input consumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%sscreen pinning is active, trying to use default input consumer",
+                        SUBSTRING_PREFIX);
                 base = getDefaultInputConsumer(reasonString);
             }
 
             if (mDeviceState.canTriggerOneHandedAction(event)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("gesture can trigger one handed mode")
-                        .append(", using OneHandedModeInputConsumer");
+                reasonString.append("%s%s%sgesture can trigger one handed mode, "
+                                + "using OneHandedModeInputConsumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = new OneHandedModeInputConsumer(
                         this, mDeviceState, base, mInputMonitorCompat);
             }
@@ -1273,15 +1322,17 @@
     }
 
     private CompoundString newCompoundString(String substring) {
-        return new CompoundString(NEWLINE_PREFIX).append(substring);
+        return new CompoundString("%s%s", NEWLINE_PREFIX, substring);
+    }
+
+    private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
+        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));
+        ActiveGestureProtoLogProxy.logSetInputConsumer(consumer.getName(), reasonString.toString());
         if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
             ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER);
         }
@@ -1298,14 +1349,12 @@
             CompoundString reasonString) {
         if (mDeviceState.isKeyguardShowingOccluded()) {
             // This handles apps showing over the lockscreen (e.g. camera)
-            return createDeviceLockedInputConsumer(
-                    gestureState,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("keyguard is showing occluded")
-                            .append(", trying to use device locked input consumer"));
+            return createDeviceLockedInputConsumer(gestureState, reasonString.append(
+                    "%skeyguard is showing occluded, trying to use device locked input consumer",
+                    SUBSTRING_PREFIX));
         }
 
-        reasonString.append(SUBSTRING_PREFIX).append("keyguard is not showing occluded");
+        reasonString.append("%skeyguard is not showing occluded", SUBSTRING_PREFIX);
 
         TopTaskTracker.CachedTaskInfo runningTask = gestureState.getRunningTask();
         // Use overview input consumer for sharesheets on top of home.
@@ -1319,11 +1368,8 @@
                 ? 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"));
+            ActiveGestureProtoLogProxy.logUpdateGestureStateRunningTask(
+                    otherVisibleTask.getPackageName(), runningTask.getPackageName());
             gestureState.updateRunningTask(otherVisibleTask);
         }
 
@@ -1349,11 +1395,12 @@
                     gestureState,
                     event,
                     forceOverviewInputConsumer,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("is in live tile mode, trying to use overview input consumer"));
+                    reasonString.append(
+                            "%sis in live tile mode, trying to use overview input consumer",
+                            SUBSTRING_PREFIX));
         } else if (runningTask == null) {
-            return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX)
-                    .append("running task == null"));
+            return getDefaultInputConsumer(reasonString.append(
+                    "%srunning task == null", SUBSTRING_PREFIX));
         } else if (previousGestureAnimatedToLauncher
                 || launcherResumedThroughShellTransition
                 || forceOverviewInputConsumer) {
@@ -1362,28 +1409,32 @@
                     gestureState,
                     event,
                     forceOverviewInputConsumer,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append(previousGestureAnimatedToLauncher
-                                    ? "previous gesture animated to launcher"
+                    reasonString.append(previousGestureAnimatedToLauncher
+                                    ? "%sprevious gesture animated to launcher, "
+                                            + "trying to use overview input consumer"
                                     : (launcherResumedThroughShellTransition
-                                            ? "launcher resumed through a shell transition"
-                                            : "forceOverviewInputConsumer == true"))
-                            .append(", trying to use overview input consumer"));
+                                            ? "%slauncher resumed through a shell transition, "
+                                                    + "trying to use overview input consumer"
+                                            : "%sforceOverviewInputConsumer == true, "
+                                                    + "trying to use overview input consumer"),
+                            SUBSTRING_PREFIX));
         } else if (mDeviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) {
-            return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX)
-                    .append(launcherChildActivityResumed
-                            ? "is launcher child-task, trying to use default input consumer"
-                            : "is gesture-blocked task, trying to use default input consumer"));
+            return getDefaultInputConsumer(reasonString.append(launcherChildActivityResumed
+                    ? "%sis launcher child-task, trying to use default input consumer"
+                    : "%sis gesture-blocked task, trying to use default input consumer",
+                    SUBSTRING_PREFIX));
         } else {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("using OtherActivityInputConsumer");
+            reasonString.append("%susing OtherActivityInputConsumer", SUBSTRING_PREFIX);
             return createOtherActivityInputConsumer(gestureState, event);
         }
     }
 
     public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
-        return !mOverviewComponentObserver.isHomeAndOverviewSame()
-                ? mFallbackSwipeHandlerFactory : mLauncherSwipeHandlerFactory;
+        boolean recentsInWindow =
+                Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
+        return mOverviewComponentObserver.isHomeAndOverviewSame()
+                ? mLauncherSwipeHandlerFactory : (recentsInWindow
+                ? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
     }
 
     private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
@@ -1402,20 +1453,18 @@
             GestureState gestureState, CompoundString reasonString) {
         if ((mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture())
                 && gestureState.getRunningTask() != null) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("device is in gesture nav mode or 3-button mode with a trackpad")
-                    .append(" gesture and running task != null")
-                    .append(", using DeviceLockedInputConsumer");
+            reasonString.append("%sdevice is in gesture nav mode or 3-button mode with a trackpad "
+                    + "gesture and running task != null, using DeviceLockedInputConsumer",
+                    SUBSTRING_PREFIX);
             return new DeviceLockedInputConsumer(
                     this, mDeviceState, mTaskAnimationManager, gestureState, mInputMonitorCompat);
         } else {
-            return getDefaultInputConsumer(reasonString
-                    .append(SUBSTRING_PREFIX)
-                    .append((mDeviceState.isFullyGesturalNavMode()
-                                    || gestureState.isTrackpadGesture())
-                            ? "running task == null"
-                            : "device is not in gesture nav mode and it's not a trackpad gesture")
-                    .append(", trying to use default input consumer"));
+            return getDefaultInputConsumer(reasonString.append(
+                    mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture()
+                            ? "%srunning task == null, trying to use default input consumer"
+                            : "%sdevice is not in gesture nav mode and it's not a trackpad gesture,"
+                                    + " trying to use default input consumer",
+                    SUBSTRING_PREFIX));
         }
     }
 
@@ -1427,35 +1476,35 @@
             CompoundString reasonString) {
         RecentsViewContainer container = gestureState.getContainerInterface().getCreatedContainer();
         if (container == null) {
-            return getDefaultInputConsumer(
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("activity == null, trying to use default input consumer"));
+            return getDefaultInputConsumer(reasonString.append(
+                    "%sactivity == null, trying to use default input consumer", SUBSTRING_PREFIX));
         }
 
-        boolean hasWindowFocus = container.getRootView().hasWindowFocus();
+        View rootview = container.getRootView();
+        boolean hasWindowFocus = rootview != null && rootview.hasWindowFocus();
         boolean isPreviousGestureAnimatingToLauncher =
                 previousGestureState.isRunningAnimationToLauncher()
                         || mDeviceState.isPredictiveBackToHomeInProgress();
         boolean isInLiveTileMode = gestureState.getContainerInterface().isInLiveTileMode();
 
-        reasonString.append(SUBSTRING_PREFIX)
-                .append(hasWindowFocus
-                        ? "activity has window focus"
-                        : (isPreviousGestureAnimatingToLauncher
-                                ? "previous gesture is still animating to launcher"
-                                : isInLiveTileMode
-                                        ? "device is in live mode"
-                                        : "all overview focus conditions failed"));
+        reasonString.append(hasWindowFocus
+                ? "%sactivity has window focus"
+                : (isPreviousGestureAnimatingToLauncher
+                        ? "%sprevious gesture is still animating to launcher"
+                        : isInLiveTileMode
+                                ? "%sdevice is in live mode"
+                                : "%sall overview focus conditions failed"), SUBSTRING_PREFIX);
         if (hasWindowFocus
                 || isPreviousGestureAnimatingToLauncher
                 || isInLiveTileMode) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("overview should have focus, using OverviewInputConsumer");
+            reasonString.append(
+                    "%soverview should have focus, using OverviewInputConsumer", SUBSTRING_PREFIX);
             return new OverviewInputConsumer(gestureState, container, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
-            reasonString.append(SUBSTRING_PREFIX).append(
-                    "overview shouldn't have focus, using OverviewWithoutFocusInputConsumer");
+            reasonString.append(
+                    "%soverview shouldn't have focus, using OverviewWithoutFocusInputConsumer",
+                    SUBSTRING_PREFIX);
             final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
             return new OverviewWithoutFocusInputConsumer(container.asContext(), mDeviceState,
                     gestureState, mInputMonitorCompat, disableHorizontalSwipe);
@@ -1492,12 +1541,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;
@@ -1527,11 +1578,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.
@@ -1541,8 +1592,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);
     }
 
@@ -1551,18 +1601,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()
@@ -1601,25 +1651,25 @@
         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);
         RecentsModel.INSTANCE.get(this).dump("", pw);
-        TopTaskTracker.INSTANCE.get(this).dump("", pw);
         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);
-        pw.println("AssistStateManager:");
-        AssistStateManager.INSTANCE.get(this).dump("\t", pw);
+        mDesktopVisibilityController.dumpLogs("", pw);
+        pw.println("ContextualSearchStateManager:");
+        ContextualSearchStateManager.INSTANCE.get(this).dump("\t", pw);
         SystemUiProxy.INSTANCE.get(this).dump(pw);
         DeviceConfigWrapper.get().dump("   ", pw);
     }
@@ -1637,4 +1687,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
new file mode 100644
index 0000000..6a72537
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 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.contextualeducation;
+
+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 final SystemUiProxy mSystemUiProxy;
+
+    @Inject
+    public SystemContextualEduStatsManager(SystemUiProxy systemUiProxy) {
+        mSystemUiProxy = systemUiProxy;
+    }
+
+    @Override
+    public void updateEduStats(boolean isTrackpadGesture, GestureType gestureType) {
+        mSystemUiProxy.updateContextualEduStats(isTrackpadGesture,
+                gestureType.name());
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
new file mode 100644
index 0000000..9f6360b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
@@ -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.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 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
new file mode 100644
index 0000000..b2670e8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -0,0 +1,40 @@
+/*
+ * 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.dagger;
+
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+import com.android.launcher3.model.WellbeingModel;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AsyncClockEventDelegate;
+
+/**
+ * Launcher Quickstep base component for Dagger injection.
+ *
+ * This class is not actually annotated as a Dagger component, since it is not used directly as one.
+ * Doing so generates unnecessary code bloat.
+ *
+ * See {@link LauncherAppComponent} for the one actually used.
+ */
+public interface QuickstepBaseAppComponent extends LauncherBaseAppComponent {
+
+    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 f4a2738..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();
     }
 
@@ -253,11 +263,14 @@
         }
 
         setFreezeViewVisibility(true);
+        if (mContainer.getDesktopVisibilityController() != null) {
+            mContainer.getDesktopVisibilityController().onLauncherStateChanged(toState);
+        }
     }
 
     @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();
         }
@@ -280,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));
         }
@@ -309,7 +324,7 @@
     }
 
     @Override
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         return !mContainer.isInState(OVERVIEW_SPLIT_SELECT);
     }
 
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
index 29c3dc8..a2884b6 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
@@ -20,13 +20,12 @@
 
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * Drag layer for fallback recents activity
  */
-public class RecentsDragLayer extends BaseDragLayer<RecentsActivity> {
-
+public class RecentsDragLayer<T extends Context & RecentsViewContainer> extends BaseDragLayer<T> {
     public RecentsDragLayer(Context context, AttributeSet attrs) {
         super(context, attrs, 1 /* alphaChannelCount */);
     }
@@ -34,8 +33,8 @@
     @Override
     public void recreateControllers() {
         mControllers = new TouchController[] {
-                new RecentsTaskController(mActivity),
-                new FallbackNavBarTouchController(mActivity),
+                new RecentsTaskController(mContainer),
+                new FallbackNavBarTouchController(mContainer),
         };
     }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index ca9753f..34783c7 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.fallback;
 
+import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
 import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
 import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
@@ -42,6 +43,7 @@
     private static final int FLAG_LIVE_TILE = BaseState.getFlag(6);
     private static final int FLAG_RECENTS_VIEW_VISIBLE = BaseState.getFlag(7);
     private static final int FLAG_TASK_THUMBNAIL_SPLASH = BaseState.getFlag(8);
+    private static final int FLAG_DETACH_DESKTOP_CAROUSEL = BaseState.getFlag(9);
 
     public static final RecentsState DEFAULT = new RecentsState(0,
             FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID
@@ -51,8 +53,8 @@
                     | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE);
     public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2,
             FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN
-                    | FLAG_RECENTS_VIEW_VISIBLE
-                    | FLAG_TASK_THUMBNAIL_SPLASH);
+                    | FLAG_RECENTS_VIEW_VISIBLE | FLAG_TASK_THUMBNAIL_SPLASH
+                    | FLAG_DETACH_DESKTOP_CAROUSEL);
     public static final RecentsState HOME = new RecentsState(3, 0);
     public static final RecentsState BG_LAUNCHER = new LauncherState(4, 0);
     public static final RecentsState OVERVIEW_SPLIT_SELECT = new RecentsState(5,
@@ -149,6 +151,11 @@
         return hasFlag(FLAG_TASK_THUMBNAIL_SPLASH);
     }
 
+    @Override
+    public boolean detachDesktopCarousel() {
+        return hasFlag(FLAG_DETACH_DESKTOP_CAROUSEL) && enableDesktopWindowingCarouselDetach();
+    }
+
     /**
      * True if the state has overview panel visible.
      */
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
index 2cb398c..07da379 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
@@ -15,18 +15,22 @@
  */
 package com.android.quickstep.fallback;
 
+import android.content.Context;
+
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
-import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.views.RecentsViewContainer;
 
-public class RecentsTaskController extends TaskViewTouchController<RecentsActivity> {
-
-    public RecentsTaskController(RecentsActivity activity) {
-        super(activity);
+public class RecentsTaskController<T extends Context & RecentsViewContainer &
+        StatefulContainer<RecentsState>> extends TaskViewTouchController<T> {
+    public RecentsTaskController(T container) {
+        super(container);
     }
 
     @Override
     protected boolean isRecentsInteractive() {
-        return mContainer.hasWindowFocus() || mContainer.getStateManager().getState().hasLiveTile();
+        return mContainer.getRootView().hasWindowFocus()
+                || mContainer.getStateManager().getState().hasLiveTile();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
new file mode 100644
index 0000000..52a7682
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.fallback.window
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.ContextThemeWrapper
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.util.Themes
+import com.android.launcher3.views.ActivityContext
+import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.fallback.RecentsDragLayer
+
+/**
+ * Window context for the Overview overlays.
+ * <p>
+ * Overlays have their own window and need a window context.
+ */
+open class RecentsWindowContext(windowContext: Context) :
+    ContextThemeWrapper(windowContext, Themes.getActivityThemeRes(windowContext)), ActivityContext {
+
+    private var deviceProfile: DeviceProfile? = null
+    private var dragLayer: RecentsDragLayer<RecentsWindowManager> = RecentsDragLayer(this, null)
+    private val deviceProfileChangeListeners:
+        MutableList<DeviceProfile.OnDeviceProfileChangeListener> =
+        ArrayList()
+
+    private val windowTitle: String = "RecentsWindow"
+
+    protected var windowLayoutParams: WindowManager.LayoutParams? =
+        createDefaultWindowLayoutParams(
+            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, windowTitle)
+
+    override fun getDragLayer(): BaseDragLayer<RecentsWindowManager> {
+        return dragLayer
+    }
+
+    override fun getDeviceProfile(): DeviceProfile {
+        if (deviceProfile == null) {
+            deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
+                .copy(this)
+        }
+        return deviceProfile!!
+    }
+
+    override fun getOnDeviceProfileChangeListeners():
+        List<DeviceProfile.OnDeviceProfileChangeListener> {
+        return deviceProfileChangeListeners
+    }
+
+    /**
+     * Creates LayoutParams for adding a view directly to WindowManager as a new window.
+     *
+     * @param type The window type to pass to the created WindowManager.LayoutParams.
+     * @param title The window title to pass to the created WindowManager.LayoutParams.
+     */
+    fun createDefaultWindowLayoutParams(type: Int, title: String): WindowManager.LayoutParams {
+        var windowFlags =
+            (WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
+                WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
+                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
+
+        val windowLayoutParams =
+            WindowManager.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                type,
+                windowFlags,
+                PixelFormat.TRANSLUCENT,
+            )
+
+        windowLayoutParams.title = title
+        windowLayoutParams.fitInsetsTypes = 0
+        windowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+        windowLayoutParams.isSystemApplicationOverlay = true
+        windowLayoutParams.privateFlags = PRIVATE_FLAG_CONSUME_IME_INSETS
+
+        return windowLayoutParams
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
new file mode 100644
index 0000000..843ef6c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.fallback.window
+
+import android.animation.AnimatorSet
+import android.app.ActivityOptions
+import android.content.ComponentName
+import android.content.Context
+import android.content.LocusId
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import android.window.RemoteTransition
+import com.android.launcher3.BaseActivity
+import com.android.launcher3.LauncherAnimationRunner
+import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory
+import com.android.launcher3.R
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statemanager.StateManager
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory
+import com.android.launcher3.statemanager.StatefulContainer
+import com.android.launcher3.taskbar.TaskbarUIController
+import com.android.launcher3.util.ContextTracker
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.RunnableList
+import com.android.launcher3.util.SystemUiController
+import com.android.launcher3.views.BaseDragLayer
+import com.android.launcher3.views.ScrimView
+import com.android.quickstep.FallbackWindowInterface
+import com.android.quickstep.OverviewComponentObserver
+import com.android.quickstep.RecentsAnimationCallbacks
+import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener
+import com.android.quickstep.RecentsAnimationController
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.RemoteAnimationTargets
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.fallback.FallbackRecentsStateController
+import com.android.quickstep.fallback.FallbackRecentsView
+import com.android.quickstep.fallback.RecentsDragLayer
+import com.android.quickstep.fallback.RecentsState
+import com.android.quickstep.fallback.RecentsState.BACKGROUND_APP
+import com.android.quickstep.fallback.RecentsState.BG_LAUNCHER
+import com.android.quickstep.fallback.RecentsState.DEFAULT
+import com.android.quickstep.fallback.RecentsState.HOME
+import com.android.quickstep.fallback.RecentsState.MODAL_TASK
+import com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT
+import com.android.quickstep.util.RecentsAtomicAnimationFactory
+import com.android.quickstep.util.RecentsWindowProtoLogProxy
+import com.android.quickstep.util.SplitSelectStateController
+import com.android.quickstep.util.TISBindHelper
+import com.android.quickstep.views.OverviewActionsView
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import java.util.function.Predicate
+
+/**
+ * Class that will manage RecentsView lifecycle within a window and interface correctly where
+ * needed. This allows us to run RecentsView in a window where needed.
+ *
+ * todo: b/365776320,b/365777482
+ *
+ * To add new protologs, see [RecentsWindowProtoLogProxy]. To enable logging to logcat, see
+ * [QuickstepProtoLogGroup.Constants.DEBUG_RECENTS_WINDOW]
+ */
+class RecentsWindowManager(context: Context) :
+    RecentsWindowContext(context), RecentsViewContainer, StatefulContainer<RecentsState> {
+
+    companion object {
+        private const val HOME_APPEAR_DURATION: Long = 250
+        private const val TAG = "RecentsWindowManager"
+
+        class RecentsWindowTracker : ContextTracker<RecentsWindowManager?>() {
+            override fun isHomeStarted(context: RecentsWindowManager?): Boolean {
+                return true
+            }
+        }
+
+        @JvmStatic val recentsWindowTracker = RecentsWindowTracker()
+    }
+
+    protected var recentsView: FallbackRecentsView<RecentsWindowManager>? = null
+    private val windowContext: Context = createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+    private val windowManager: WindowManager =
+        windowContext.getSystemService(WindowManager::class.java)!!
+    private var layoutInflater: LayoutInflater = LayoutInflater.from(this).cloneInContext(this)
+    private var stateManager: StateManager<RecentsState, RecentsWindowManager> =
+        StateManager<RecentsState, RecentsWindowManager>(this, RecentsState.BG_LAUNCHER)
+    private var mSystemUiController: SystemUiController? = null
+
+    private var dragLayer: RecentsDragLayer<RecentsWindowManager>? = null
+    private var windowView: View? = null
+    private var actionsView: OverviewActionsView<*>? = null
+    private var scrimView: ScrimView? = null
+
+    private var callbacks: RecentsAnimationCallbacks? = null
+
+    private var taskbarUIController: TaskbarUIController? = null
+    private var tisBindHelper: TISBindHelper = TISBindHelper(this) {}
+
+    // Callback array that corresponds to events defined in @ActivityEvent
+    private val mEventCallbacks =
+        listOf(RunnableList(), RunnableList(), RunnableList(), RunnableList())
+    private var onInitListener: Predicate<Boolean>? = null
+
+    private val taskStackChangeListener =
+        object : TaskStackChangeListener {
+            override fun onTaskMovedToFront(taskId: Int) {
+                if ((isShowing() && isInState(DEFAULT))) {
+                    // handling state where we end recents animation by swiping livetile away
+                    // TODO: animate this switch.
+                    cleanupRecentsWindow()
+                }
+            }
+        }
+
+    private val recentsAnimationListener =
+        object : RecentsAnimationListener {
+            override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
+                recentAnimationStopped()
+            }
+
+            override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
+                recentAnimationStopped()
+            }
+        }
+
+    init {
+        FallbackWindowInterface.init(this)
+        TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
+    }
+
+    override fun destroy() {
+        super.destroy()
+        cleanupRecentsWindow()
+        FallbackWindowInterface.getInstance()?.destroy()
+        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
+        callbacks?.removeListener(recentsAnimationListener)
+        recentsWindowTracker.onContextDestroyed(this)
+    }
+
+    override fun startHome() {
+        val recentsView: RecentsView<*, *> = getOverviewPanel()
+        recentsView.switchToScreenshot {
+            recentsView.finishRecentsAnimation(true) { startHomeInternal() }
+        }
+    }
+
+    private fun startHomeInternal() {
+        val runner = LauncherAnimationRunner(mainThreadHandler, mAnimationToHomeFactory, true)
+        val options =
+            ActivityOptions.makeRemoteAnimation(
+                RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
+                RemoteTransition(
+                    runner.toRemoteTransition(),
+                    iApplicationThread,
+                    "StartHomeFromRecents",
+                ),
+            )
+        OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
+        stateManager.moveToRestState()
+    }
+
+    private val mAnimationToHomeFactory =
+        RemoteAnimationFactory {
+            _: Int,
+            appTargets: Array<RemoteAnimationTarget>?,
+            wallpaperTargets: Array<RemoteAnimationTarget>?,
+            nonAppTargets: Array<RemoteAnimationTarget>?,
+            result: LauncherAnimationRunner.AnimationResult? ->
+            val controller =
+                getStateManager().createAnimationToNewWorkspace(BG_LAUNCHER, HOME_APPEAR_DURATION)
+            controller.dispatchOnStart()
+            val targets =
+                RemoteAnimationTargets(
+                    appTargets,
+                    wallpaperTargets,
+                    nonAppTargets,
+                    RemoteAnimationTarget.MODE_OPENING,
+                )
+            for (app in targets.apps) {
+                SurfaceControl.Transaction().setAlpha(app.leash, 1f).apply()
+            }
+            val anim = AnimatorSet()
+            anim.play(controller.animationPlayer)
+            anim.setDuration(HOME_APPEAR_DURATION)
+            result!!.setAnimation(
+                anim,
+                this@RecentsWindowManager,
+                {
+                    getStateManager().goToState(BG_LAUNCHER, false)
+                    cleanupRecentsWindow()
+                },
+                true, /* skipFirstFrame */
+            )
+        }
+
+    private val onBackInvokedCallback: () -> Unit = {
+        // If we are in live tile mode, launch the live task, otherwise return home
+        recentsView?.runningTaskView?.launchWithAnimation() ?: startHome()
+    }
+
+    private fun cleanupRecentsWindow() {
+        RecentsWindowProtoLogProxy.logCleanup(isShowing())
+        if (isShowing()) {
+            windowManager.removeViewImmediate(windowView)
+        }
+        stateManager.moveToRestState()
+        callbacks?.removeListener(recentsAnimationListener)
+    }
+
+    private fun isShowing(): Boolean {
+        return windowView?.parent != null
+    }
+
+    fun startRecentsWindow(callbacks: RecentsAnimationCallbacks? = null) {
+        RecentsWindowProtoLogProxy.logStartRecentsWindow(isShowing(), windowView == null)
+        if (isShowing()) {
+            return
+        }
+        if (windowView == null) {
+            windowView = layoutInflater.inflate(R.layout.fallback_recents_activity, null)
+        }
+        windowManager.addView(windowView, windowLayoutParams)
+
+        windowView
+            ?.findOnBackInvokedDispatcher()
+            ?.registerSystemOnBackInvokedCallback(onBackInvokedCallback)
+
+        windowView?.systemUiVisibility =
+            (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
+
+        recentsView = windowView?.findViewById(R.id.overview_panel)
+        actionsView = windowView?.findViewById(R.id.overview_actions_view)
+        scrimView = windowView?.findViewById(R.id.scrim_view)
+        val systemUiProxy = SystemUiProxy.INSTANCE[this]
+        val splitSelectStateController =
+            SplitSelectStateController(
+                this,
+                getStateManager(),
+                null, /* depthController */
+                statsLogManager,
+                systemUiProxy,
+                RecentsModel.INSTANCE[this],
+                null, /*activityBackCallback*/
+            )
+        recentsView?.init(actionsView, splitSelectStateController, null)
+        dragLayer = windowView?.findViewById(R.id.drag_layer)
+
+        actionsView?.updateDimension(getDeviceProfile(), recentsView?.lastComputedTaskSize)
+        actionsView?.updateVerticalMargin(DisplayController.getNavigationMode(this))
+
+        mSystemUiController = SystemUiController(windowView)
+        recentsWindowTracker.handleCreate(this)
+
+        this.callbacks = callbacks
+        callbacks?.addListener(recentsAnimationListener)
+    }
+
+    private fun recentAnimationStopped() {
+        if (isInState(BACKGROUND_APP)) {
+            cleanupRecentsWindow()
+        }
+    }
+
+    override fun getComponentName(): ComponentName {
+        return ComponentName(this, RecentsWindowManager::class.java)
+    }
+
+    override fun canStartHomeSafely(): Boolean {
+        val overviewCommandHelper = tisBindHelper.overviewCommandHelper
+        return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely()
+    }
+
+    override fun getDesktopVisibilityController(): DesktopVisibilityController? {
+        return tisBindHelper.desktopVisibilityController
+    }
+
+    override fun setTaskbarUIController(taskbarUIController: TaskbarUIController?) {
+        this.taskbarUIController = taskbarUIController
+    }
+
+    override fun getTaskbarUIController(): TaskbarUIController? {
+        return taskbarUIController
+    }
+
+    override fun getTISBindHelper(): TISBindHelper {
+        return tisBindHelper
+    }
+
+    fun registerInitListener(onInitListener: Predicate<Boolean>) {
+        this.onInitListener = onInitListener
+    }
+
+    override fun collectStateHandlers(out: MutableList<StateManager.StateHandler<RecentsState?>>?) {
+        out!!.add(FallbackRecentsStateController(this))
+    }
+
+    override fun getStateManager(): StateManager<RecentsState, RecentsWindowManager> {
+        return this.stateManager
+    }
+
+    override fun shouldAnimateStateChange(): Boolean {
+        return true
+    }
+
+    override fun isInState(state: RecentsState?): Boolean {
+        return stateManager.state == state
+    }
+
+    override fun onStateSetStart(state: RecentsState) {
+        super.onStateSetStart(state)
+        RecentsWindowProtoLogProxy.logOnStateSetStart(getStateName(state))
+    }
+
+    override fun onStateSetEnd(state: RecentsState) {
+        super.onStateSetEnd(state)
+        RecentsWindowProtoLogProxy.logOnStateSetEnd(getStateName(state))
+
+        if (state == HOME || state == BG_LAUNCHER) {
+            cleanupRecentsWindow()
+        }
+    }
+
+    private fun getStateName(state: RecentsState?): String {
+        return when (state) {
+            null -> "NULL"
+            DEFAULT -> "default"
+            MODAL_TASK -> "MODAL_TASK"
+            BACKGROUND_APP -> "BACKGROUND_APP"
+            HOME -> "HOME"
+            BG_LAUNCHER -> "BG_LAUNCHER"
+            OVERVIEW_SPLIT_SELECT -> "OVERVIEW_SPLIT_SELECT"
+            else -> "ordinal=" + state.ordinal
+        }
+    }
+
+    override fun getSystemUiController(): SystemUiController? {
+        if (mSystemUiController == null) {
+            mSystemUiController = SystemUiController(rootView)
+        }
+        return mSystemUiController
+    }
+
+    override fun getContext(): Context {
+        return this
+    }
+
+    override fun getScrimView(): ScrimView? {
+        return scrimView
+    }
+
+    override fun <T : View?> getOverviewPanel(): T {
+        return recentsView as T
+    }
+
+    override fun getRootView(): View? {
+        return windowView
+    }
+
+    override fun getDragLayer(): BaseDragLayer<RecentsWindowManager> {
+        return dragLayer!!
+    }
+
+    override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
+        // TODO(b/368610710)
+        return false
+    }
+
+    override fun dispatchKeyEvent(ev: KeyEvent?): Boolean {
+        // TODO(b/368610710)
+        return false
+    }
+
+    override fun getActionsView(): OverviewActionsView<*>? {
+        return actionsView
+    }
+
+    override fun addForceInvisibleFlag(flag: Int) {}
+
+    override fun clearForceInvisibleFlag(flag: Int) {}
+
+    override fun setLocusContext(id: LocusId?, bundle: Bundle?) {
+        // no op
+    }
+
+    override fun isStarted(): Boolean {
+        return isShowing() && isInState(DEFAULT)
+    }
+
+    /** Adds a callback for the provided activity event */
+    override fun addEventCallback(@BaseActivity.ActivityEvent event: Int, callback: Runnable?) {
+        mEventCallbacks[event].add(callback)
+    }
+
+    /** Removes a previously added callback */
+    override fun removeEventCallback(@BaseActivity.ActivityEvent event: Int, callback: Runnable?) {
+        mEventCallbacks[event].remove(callback)
+    }
+
+    override fun runOnBindToTouchInteractionService(r: Runnable?) {
+        tisBindHelper.runOnBindToTouchInteractionService(r)
+    }
+
+    override fun addMultiWindowModeChangedListener(
+        listener: BaseActivity.MultiWindowModeChangedListener?
+    ) {
+        // TODO(b/368408838)
+    }
+
+    override fun removeMultiWindowModeChangedListener(
+        listener: BaseActivity.MultiWindowModeChangedListener?
+    ) {}
+
+    override fun returnToHomescreen() {
+        startHome()
+    }
+
+    override fun isRecentsViewVisible(): Boolean {
+        return getStateManager().state!!.isRecentsViewVisible
+    }
+
+    override fun createAtomicAnimationFactory(): AtomicAnimationFactory<RecentsState?>? {
+        return RecentsAtomicAnimationFactory<RecentsWindowManager, RecentsState>(this)
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
new file mode 100644
index 0000000..4f9d837
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback.window;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import static com.android.app.animation.Interpolators.ACCELERATE;
+import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE;
+import static com.android.launcher3.GestureNavContract.EXTRA_ON_FINISH_CALLBACK;
+import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.RemoteAnimationTarget;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.AbsSwipeUpHandler;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.system.InputConsumerController;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+/**
+ * Handles the navigation gestures when a 3rd party launcher is the default home activity.
+ *
+ * Bugs: b/365775417
+ */
+public class RecentsWindowSwipeHandler extends AbsSwipeUpHandler<RecentsWindowManager,
+        FallbackRecentsView<RecentsWindowManager>, RecentsState> {
+
+    private static final String TAG = "RecentsWindowSwipeHandler";
+
+    /**
+     * Message used for receiving gesture nav contract information. We use a static messenger to
+     * avoid leaking too make binders in case the receiving launcher does not handle the contract
+     * properly.
+     */
+    private static StaticMessageReceiver sMessageReceiver = null;
+
+    private FallbackHomeAnimationFactory mActiveAnimationFactory;
+    private final boolean mRunningOverHome;
+
+    private final Matrix mTmpMatrix = new Matrix();
+    private float mMaxLauncherScale = 1;
+
+    private boolean mAppCanEnterPip;
+
+    public RecentsWindowSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+            boolean continuingLastGesture, InputConsumerController inputConsumer,
+            RecentsWindowManager recentsWindowManager) {
+        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+                continuingLastGesture, inputConsumer, recentsWindowManager);
+
+        mRunningOverHome = mGestureState.getRunningTask() != null
+                && mGestureState.getRunningTask().isHomeTask();
+
+        initTransformParams();
+    }
+
+    @Override
+    public void onRecentsAnimationStart(RecentsAnimationController controller,
+            RecentsAnimationTargets targets) {
+        super.onRecentsAnimationStart(controller, targets);
+        initTransformParams();
+    }
+
+    private void initTransformParams() {
+        if (mActiveAnimationFactory != null) {
+            mActiveAnimationFactory.initTransformParams();
+            return;
+        }
+        runActionOnRemoteHandles(remoteTargetHandle ->
+                remoteTargetHandle.getTransformParams().setHomeBuilderProxy(
+                        RecentsWindowSwipeHandler.this::updateHomeActivityTransformDuringSwipeUp));
+    }
+
+    @Override
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        super.initTransitionEndpoints(dp);
+        if (mRunningOverHome) {
+            // Full screen scale should be independent of remote target handle
+            mMaxLauncherScale = 1 / mRemoteTargetHandles[0].getTaskViewSimulator()
+                    .getFullScreenScale();
+        }
+    }
+
+    private void updateHomeActivityTransformDuringSwipeUp(SurfaceProperties builder,
+            RemoteAnimationTarget app, TransformParams params) {
+        if (mActiveAnimationFactory != null) {
+            return;
+        }
+        setHomeScaleAndAlpha(builder, app, mCurrentShift.value, 0);
+    }
+
+    private void setHomeScaleAndAlpha(SurfaceProperties builder,
+            RemoteAnimationTarget app, float verticalShift, float alpha) {
+        if (app.windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME) {
+            return;
+        }
+        float scale = Utilities.mapRange(verticalShift, 1, mMaxLauncherScale);
+        mTmpMatrix.setScale(scale, scale,
+                app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
+        builder.setMatrix(mTmpMatrix).setAlpha(alpha);
+        builder.setShow();
+    }
+
+    @Override
+    protected HomeAnimationFactory createHomeAnimationFactory(
+            List<IBinder> launchCookies,
+            long duration,
+            boolean isTargetTranslucent,
+            boolean appCanEnterPip,
+            RemoteAnimationTarget runningTaskTarget,
+            @Nullable TaskView targetTaskView) {
+        mAppCanEnterPip = appCanEnterPip;
+        if (appCanEnterPip) {
+            return new FallbackPipToHomeAnimationFactory();
+        }
+        mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
+        //todo: b/368410893 follow up on this as its intent focused and seems to cut immediately
+        Intent intent = new Intent(mGestureState.getHomeIntent());
+        if (runningTaskTarget != null) {
+            mActiveAnimationFactory.addGestureContract(intent, runningTaskTarget.taskInfo);
+        }
+        return mActiveAnimationFactory;
+    }
+
+    @Override
+    protected void finishRecentsControllerToHome(Runnable callback) {
+        final Runnable recentsCallback;
+        if (mAppCanEnterPip) {
+            // Make sure Launcher is resumed after auto-enter-pip transition to actually trigger
+            // the PiP task appearing.
+            recentsCallback = () -> {
+                callback.run();
+                mRecentsWindowManager.startHome();
+            };
+        } else {
+            recentsCallback = callback;
+        }
+        mRecentsView.cleanupRemoteTargets();
+        mRecentsAnimationController.finish(
+                mAppCanEnterPip /* toRecents */, recentsCallback, true /* sendUserLeaveHint */);
+    }
+
+    @Override
+    protected void switchToScreenshot() {
+        if (mRunningOverHome) {
+            // When the current task is home, then we don't need to capture anything
+            mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+        } else {
+            super.switchToScreenshot();
+        }
+    }
+
+    @Override
+    protected void notifyGestureAnimationStartToRecents() {
+        if (mRunningOverHome) {
+            if (DisplayController.getNavigationMode(mContext).hasGestures) {
+                mRecentsView.onGestureAnimationStartOnHome(
+                        mGestureState.getRunningTask().getPlaceholderTasks(),
+                        mDeviceState.getRotationTouchHelper());
+            }
+        } else {
+            super.notifyGestureAnimationStartToRecents();
+        }
+    }
+
+    private class FallbackPipToHomeAnimationFactory extends HomeAnimationFactory {
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            // copied from {@link LauncherSwipeHandlerV2.LauncherHomeAnimationFactory}
+            long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
+            return mContainer.getStateManager().createAnimationToNewWorkspace(
+                    RecentsState.HOME, accuracy, StateAnimationConfig.SKIP_ALL_ANIMATIONS);
+        }
+    }
+
+    private class FallbackHomeAnimationFactory extends HomeAnimationFactory
+            implements Consumer<Message> {
+        private final Rect mTempRect = new Rect();
+
+        private final TransformParams mTransformParams = new TransformParams();
+        private final AnimatedFloat mHomeAlpha = new AnimatedFloat(this::updateAppTransforms);
+        private final AnimatedFloat mVerticalShiftForScale =
+                new AnimatedFloat(this::updateAppTransforms);
+        private final AnimatedFloat mRecentsAlpha = new AnimatedFloat(this:: updateAppTransforms);
+
+        private final RectF mTargetRect = new RectF();
+        private SurfaceControl mSurfaceControl;
+
+        private boolean mAnimationFinished;
+        private Message mOnFinishCallback;
+
+        private final long mDuration;
+
+        private RectFSpringAnim mSpringAnim;
+        FallbackHomeAnimationFactory(long duration) {
+            mDuration = duration;
+
+            if (mRunningOverHome) {
+                mVerticalShiftForScale.value = mCurrentShift.value;
+            }
+            mRecentsAlpha.value = 1;
+            mHomeAlpha.value = 0;
+
+            initTransformParams();
+        }
+
+        @NonNull
+        @Override
+        public RectF getWindowTargetRect() {
+            if (mTargetRect.isEmpty()) {
+                mTargetRect.set(super.getWindowTargetRect());
+            }
+            return mTargetRect;
+        }
+
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            PendingAnimation pa = new PendingAnimation(mDuration);
+            pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCELERATE);
+            pa.setFloat(mHomeAlpha, AnimatedFloat.VALUE, 1, ACCELERATE);
+            return pa.createPlaybackController();
+        }
+
+        @Override
+        public void playAtomicAnimation(float velocity) {
+            if (!mRunningOverHome) {
+                return;
+            }
+            // Spring back launcher scale
+            new SpringAnimationBuilder(mContext)
+                    .setStartValue(mVerticalShiftForScale.value)
+                    .setEndValue(0)
+                    .setStartVelocity(-velocity / mTransitionDragLength)
+                    .setMinimumVisibleChange(1f / mDp.heightPx)
+                    .setDampingRatio(0.6f)
+                    .setStiffness(800)
+                    .build(mVerticalShiftForScale, AnimatedFloat.VALUE)
+                    .start();
+        }
+
+        @Override
+        public void setAnimation(RectFSpringAnim anim) {
+            mSpringAnim = anim;
+            mSpringAnim.addAnimatorListener(forEndCallback(this::onRectAnimationEnd));
+        }
+
+        private void initTransformParams() {
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTransformParams().setHomeBuilderProxy(
+                            FallbackHomeAnimationFactory.this
+                                    ::updateHomeActivityTransformDuringHomeAnim));
+
+            mTransformParams.setTargetSet(mRecentsAnimationTargets);
+        }
+
+        private void updateRecentsActivityTransformDuringHomeAnim(SurfaceProperties builder,
+                RemoteAnimationTarget app, TransformParams params) {
+            if (app.mode != mRecentsAnimationTargets.targetMode) {
+                return;
+            }
+            builder.setAlpha(mRecentsAlpha.value);
+        }
+
+        private void updateAppTransforms() {
+            mTransformParams.applySurfaceParams(
+                    mTransformParams.createSurfaceParams(FallbackHomeAnimationFactory.this
+                            ::updateRecentsActivityTransformDuringHomeAnim));
+        }
+
+        private void updateHomeActivityTransformDuringHomeAnim(SurfaceProperties builder,
+                RemoteAnimationTarget app, TransformParams params) {
+            setHomeScaleAndAlpha(builder, app, mVerticalShiftForScale.value, mHomeAlpha.value);
+        }
+
+        private void onRectAnimationEnd() {
+            mAnimationFinished = true;
+            maybeSendEndMessage();
+        }
+
+        private void maybeSendEndMessage() {
+            if (mAnimationFinished && mOnFinishCallback != null) {
+                try {
+                    mOnFinishCallback.replyTo.send(mOnFinishCallback);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error sending icon position", e);
+                }
+            }
+        }
+
+        @Override
+        public void accept(Message msg) {
+            try {
+                Bundle data = msg.getData();
+                RectF position = data.getParcelable(EXTRA_ICON_POSITION);
+                if (!position.isEmpty()) {
+                    mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE);
+                    mTargetRect.set(position);
+                    if (mSpringAnim != null) {
+                        mSpringAnim.onTargetPositionChanged();
+                    }
+                }
+                mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK);
+                maybeSendEndMessage();
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+
+        @Override
+        public void update(RectF currentRect, float progress, float radius, int overlayAlpha) {
+            if (mSurfaceControl != null) {
+                currentRect.roundOut(mTempRect);
+                Transaction t = new Transaction();
+                try {
+                    t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+                    t.apply();
+                } catch (RuntimeException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        private void addGestureContract(Intent intent, RunningTaskInfo runningTaskInfo) {
+            if (mRunningOverHome || runningTaskInfo == null) {
+                return;
+            }
+
+            TaskKey key = new TaskKey(runningTaskInfo);
+            if (key.getComponent() != null) {
+                if (sMessageReceiver == null) {
+                    sMessageReceiver = new StaticMessageReceiver();
+                }
+
+                Bundle gestureNavContract = new Bundle();
+                gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent());
+                gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId));
+                gestureNavContract.putParcelable(
+                        EXTRA_REMOTE_CALLBACK, sMessageReceiver.newCallback(this));
+                intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract);
+            }
+        }
+    }
+
+    private static class StaticMessageReceiver implements Handler.Callback {
+
+        private final Messenger mMessenger =
+                new Messenger(new Handler(Looper.getMainLooper(), this));
+
+        private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID());
+        private WeakReference<Consumer<Message>> mCurrentCallback = new WeakReference<>(null);
+
+        public Message newCallback(Consumer<Message> callback) {
+            mCurrentUID = new ParcelUuid(UUID.randomUUID());
+            mCurrentCallback = new WeakReference<>(callback);
+
+            Message msg = Message.obtain();
+            msg.replyTo = mMessenger;
+            msg.obj = mCurrentUID;
+            return msg;
+        }
+
+        @Override
+        public boolean handleMessage(@NonNull Message message) {
+            if (mCurrentUID.equals(message.obj)) {
+                Consumer<Message> consumer = mCurrentCallback.get();
+                if (consumer != null) {
+                    consumer.accept(message);
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
new file mode 100644
index 0000000..778c231
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.inputconsumers;
+
+import static android.view.MotionEvent.INVALID_POINTER_ID;
+
+import android.content.Context;
+import android.graphics.PointF;
+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.stashing.BubbleStashController;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Listens for touch events on the bubble bar.
+ */
+public class BubbleBarInputConsumer implements InputConsumer {
+
+    private final BubbleStashController mBubbleStashController;
+    private final BubbleBarViewController mBubbleBarViewController;
+    @Nullable
+    private final BubbleBarSwipeController mBubbleBarSwipeController;
+    private final InputMonitorCompat mInputMonitorCompat;
+
+    private boolean mPilfered;
+    private boolean mPassedTouchSlop;
+    private boolean mStashedOrCollapsedOnDown;
+
+    private final int mTouchSlop;
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private final long mTimeForTap;
+    private int mActivePointerId = INVALID_POINTER_ID;
+
+    public BubbleBarInputConsumer(Context context, BubbleControllers bubbleControllers,
+            InputMonitorCompat inputMonitorCompat) {
+        mBubbleStashController = bubbleControllers.bubbleStashController;
+        mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+        mBubbleBarSwipeController = bubbleControllers.bubbleBarSwipeController.orElse(null);
+
+        mInputMonitorCompat = inputMonitorCompat;
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mTimeForTap = ViewConfiguration.getTapTimeout();
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_BUBBLE_BAR;
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        final int action = ev.getAction();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mActivePointerId = ev.getPointerId(0);
+                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);
+                if (pointerIndex == INVALID_POINTER_ID) {
+                    break;
+                }
+                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                float dX = mLastPos.x - mDownPos.x;
+                float dY = mLastPos.y - mDownPos.y;
+                if (!mPassedTouchSlop) {
+                    mPassedTouchSlop = Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
+                }
+                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();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
+                        && mBubbleBarSwipeController.isSwipeGesture();
+                boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap;
+                if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
+                        && mStashedOrCollapsedOnDown) {
+                    // Taps on the handle / collapsed state should open the bar
+                    mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
+                }
+                break;
+        }
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            cleanupAfterMotionEvent();
+        }
+    }
+
+    private void cleanupAfterMotionEvent() {
+        if (mBubbleBarSwipeController != null) {
+            mBubbleBarSwipeController.finish();
+        }
+        mPassedTouchSlop = false;
+        mPilfered = false;
+    }
+
+    private boolean isCollapsed() {
+        return mBubbleStashController.isBubbleBarVisible()
+                && !mBubbleBarViewController.isExpanded();
+    }
+
+    /**
+     * Returns whether the event is occurring on a visible bubble bar or the bar handle.
+     */
+    public static boolean isEventOnBubbles(TaskbarActivityContext tac, MotionEvent ev) {
+        if (tac == null || !tac.isBubbleBarEnabled()) {
+            return false;
+        }
+        BubbleControllers controllers = tac.getBubbleControllers();
+        if (controllers == null || !controllers.bubbleBarViewController.hasBubbles()) {
+            return false;
+        }
+        if (controllers.bubbleStashController.isStashed()
+                && controllers.bubbleStashedHandleViewController.isPresent()) {
+            return controllers.bubbleStashedHandleViewController.get().isEventOverHandle(ev);
+        } else if (controllers.bubbleBarViewController.isBubbleBarVisible()) {
+            return controllers.bubbleBarViewController.isEventOverBubbleBar(ev);
+        }
+        return false;
+    }
+}
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/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index f264364..b66d4cb 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -24,7 +24,6 @@
 import static com.android.quickstep.AbsSwipeUpHandler.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
-import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
 import android.animation.Animator;
@@ -58,7 +57,6 @@
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.TransformParams.BuilderProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 import java.util.HashMap;
@@ -213,15 +211,13 @@
                         // This will come back and cancel the interaction.
                         startHomeIntentSafely(mContext, mGestureState.getHomeIntent(), null, TAG);
                         mHomeLaunched = true;
-                    } else if (ENABLE_SHELL_TRANSITIONS) {
-                        if (mTaskAnimationManager.getCurrentCallbacks() != null) {
-                            if (mRecentsAnimationController != null) {
-                                finishRecentsAnimationForShell(dismissTask);
-                            } else {
-                                // the transition of recents animation hasn't started, wait for it
-                                mCancelWhenRecentsStart = true;
-                                mDismissTask = dismissTask;
-                            }
+                    } else if (mTaskAnimationManager.getCurrentCallbacks() != null) {
+                        if (mRecentsAnimationController != null) {
+                            finishRecentsAnimationForShell(dismissTask);
+                        } else {
+                            // the transition of recents animation hasn't started, wait for it
+                            mCancelWhenRecentsStart = true;
+                            mDismissTask = dismissTask;
                         }
                     }
                     mStateCallback.setState(STATE_HANDLER_INVALIDATED);
@@ -278,9 +274,7 @@
     }
 
     private void endRemoteAnimation() {
-        if (mHomeLaunched) {
-            ActivityManagerWrapper.getInstance().cancelRecentsAnimation(false);
-        } else if (mRecentsAnimationController != null) {
+        if (!mHomeLaunched && mRecentsAnimationController != null) {
             mRecentsAnimationController.finishController(
                     false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
         }
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 848a43a..f5bef05e 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -27,6 +27,8 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.DisplayController;
@@ -36,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;
 
 /**
@@ -48,7 +50,7 @@
     private static final boolean DEBUG_NAV_HANDLE = Utilities.isPropertyEnabled(
             NAV_HANDLE_LONG_PRESS);
 
-    private final NavHandleLongPressHandler mNavHandleLongPressHandler;
+    private NavHandleLongPressHandler mNavHandleLongPressHandler;
     private final float mNavHandleWidth;
     private final float mScreenWidth;
 
@@ -73,17 +75,34 @@
         super(delegate, inputMonitor);
         mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
         mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress();
-        int twoStageMultiplier = DeviceConfigWrapper.get().getTwoStageMultiplier();
-        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();
         }
-        mOuterLongPressTimeout = mLongPressTimeout * twoStageMultiplier;
-        mTouchSlopSquaredOriginal = deviceState.getSquaredTouchSlop();
-        mTouchSlopSquared = mTouchSlopSquaredOriginal;
-        mOuterTouchSlopSquared = mTouchSlopSquared * (twoStageMultiplier * twoStageMultiplier);
+        float twoStageDurationMultiplier =
+                (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+        mOuterLongPressTimeout = (int) (mLongPressTimeout * twoStageDurationMultiplier);
+
+        float gestureNavTouchSlopSquared = deviceState.getSquaredTouchSlop();
+        float twoStageSlopMultiplier =
+                (DeviceConfigWrapper.get().getTwoStageSlopPercentage() / 100f);
+        float twoStageSlopMultiplierSquared = twoStageSlopMultiplier * twoStageSlopMultiplier;
+        if (DeviceConfigWrapper.get().getEnableLpnhTwoStages()) {
+            // For 2 stages, the outer touch slop should match gesture nav.
+            mTouchSlopSquared = gestureNavTouchSlopSquared * twoStageSlopMultiplierSquared;
+            mOuterTouchSlopSquared = gestureNavTouchSlopSquared;
+        } else {
+            // For single stage, the touch slop should match gesture nav.
+            mTouchSlopSquared = gestureNavTouchSlopSquared;
+            // Note: This outer slop is not actually used for single-stage (flag disabled).
+            mOuterTouchSlopSquared = gestureNavTouchSlopSquared;
+        }
+        mTouchSlopSquaredOriginal = mTouchSlopSquared;
+
         mGestureState = gestureState;
         mGestureState.setIsInExtendedSlopRegion(false);
         if (DEBUG_NAV_HANDLE) {
@@ -250,4 +269,9 @@
     protected String getDelegatorName() {
         return "NavHandleLongPressInputConsumer";
     }
+
+    @VisibleForTesting
+    void setNavHandleLongPressHandler(NavHandleLongPressHandler navHandleLongPressHandler) {
+        mNavHandleLongPressHandler = navHandleLongPressHandler;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 13b6447..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
@@ -409,6 +411,7 @@
         mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs);
         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener());
+        mMotionPauseDetector.setIsTrackpadGesture(mGestureState.isTrackpadGesture());
         mInteractionHandler.initWhenReady(
                 "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation");
 
@@ -423,7 +426,11 @@
             mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
             notifyGestureStarted(true /*isLikelyToStartNewTask*/);
         } else {
-            Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
+            // todo differentiate intent based on if we are on home or in app for overview in window
+            boolean useHomeIntentForWindow = Flags.enableFallbackOverviewInWindow()
+                    || Flags.enableLauncherOverviewInWindow();
+            Intent intent = new Intent(useHomeIntentForWindow ? mInteractionHandler.getHomeIntent()
+                : mInteractionHandler.getLaunchIntent());
             intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
             mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
                     mInteractionHandler);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index c61f71d..a236eca 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -28,6 +28,7 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.views.BaseDragLayer;
@@ -41,7 +42,8 @@
 /**
  * Input consumer for handling touch on the recents/Launcher activity.
  */
-public class OverviewInputConsumer<S extends BaseState<S>, T extends RecentsViewContainer>
+public class OverviewInputConsumer<S extends BaseState<S>,
+        T extends RecentsViewContainer & StatefulContainer<S>>
         implements InputConsumer {
 
     private final T mContainer;
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 95295b0..49bff8d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -15,9 +15,7 @@
  */
 package com.android.quickstep.inputconsumers;
 
-import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_MOVE;
-import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
 import static com.android.launcher3.Flags.enableCursorHoverStates;
@@ -43,12 +41,12 @@
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarThresholdUtils;
 import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
-import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.OverviewCommandHelper.CommandType;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
@@ -69,9 +67,6 @@
     private final int mTaskbarNavThresholdY;
     private final boolean mIsTaskbarAllAppsOpen;
     private boolean mHasPassedTaskbarNavThreshold;
-    private boolean mIsInBubbleBarArea;
-    private boolean mIsVerticalGestureOverBubbleBar;
-    private boolean mIsPassedBubbleBarSlop;
     private final int mTouchSlop;
 
     private final PointF mDownPos = new PointF();
@@ -159,9 +154,6 @@
                         if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) {
                             mTransitionCallback.onActionDown();
                         }
-                        if (mIsTransientTaskbar && isInBubbleBarArea(x)) {
-                            mIsInBubbleBarArea = true;
-                        }
                         break;
                     case MotionEvent.ACTION_POINTER_UP:
                         int ptrIdx = ev.getActionIndex();
@@ -185,18 +177,6 @@
                         float dX = mLastPos.x - mDownPos.x;
                         float dY = mLastPos.y - mDownPos.y;
 
-                        if (!mIsPassedBubbleBarSlop && mIsInBubbleBarArea) {
-                            boolean passedSlop =
-                                    Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
-                            if (passedSlop) {
-                                mIsPassedBubbleBarSlop = true;
-                                mIsVerticalGestureOverBubbleBar = Math.abs(dY) > Math.abs(dX);
-                                if (mIsVerticalGestureOverBubbleBar) {
-                                    setActive(ev);
-                                }
-                            }
-                        }
-
                         if (mIsTransientTaskbar) {
                             boolean passedTaskbarNavThreshold = dY < 0
                                     && Math.abs(dY) >= mTaskbarNavThreshold;
@@ -204,11 +184,7 @@
                             if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold
                                     && !mGestureState.isInExtendedSlopRegion()) {
                                 mHasPassedTaskbarNavThreshold = true;
-                                if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) {
-                                    mTaskbarActivityContext.onSwipeToOpenBubblebar();
-                                } else {
-                                    mTaskbarActivityContext.onSwipeToUnstashTaskbar();
-                                }
+                                mTaskbarActivityContext.onSwipeToUnstashTaskbar(true);
                             }
 
                             if (dY < 0) {
@@ -225,46 +201,13 @@
                         break;
                     case MotionEvent.ACTION_BUTTON_RELEASE:
                         if (isStashedTaskbarHovered) {
-                            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME);
+                            mOverviewCommandHelper.addCommand(CommandType.HOME);
                         }
                         break;
                 }
             }
-            boolean isMovingInBubbleBarArea = mIsInBubbleBarArea && ev.getAction() == ACTION_MOVE;
             if (!isStashedTaskbarHovered) {
-                // if we're moving in the bubble bar area but we haven't passed the slop yet, don't
-                // propagate to the delegate, until we can determine the direction of the gesture.
-                if (!isMovingInBubbleBarArea || mIsPassedBubbleBarSlop) {
-                    mDelegate.onMotionEvent(ev);
-                }
-            }
-        } else if (mIsVerticalGestureOverBubbleBar) {
-            // if we get here then this gesture is a vertical swipe over the bubble bar.
-            // we're also active and there's no need to delegate any additional motion events. the
-            // rest of the gesture will be handled here.
-            switch (ev.getAction()) {
-                case ACTION_MOVE:
-                    int pointerIndex = ev.findPointerIndex(mActivePointerId);
-                    if (pointerIndex == INVALID_POINTER_ID) {
-                        break;
-                    }
-                    mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
-
-                    float dY = mLastPos.y - mDownPos.y;
-
-                    // bubble bar swipe gesture uses the same threshold as the taskbar.
-                    boolean passedTaskbarNavThreshold = dY < 0
-                            && Math.abs(dY) >= mTaskbarNavThreshold;
-
-                    if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
-                        mHasPassedTaskbarNavThreshold = true;
-                        mTaskbarActivityContext.onSwipeToOpenBubblebar();
-                    }
-                    break;
-                case ACTION_UP:
-                case ACTION_CANCEL:
-                    cleanupAfterMotionEvent();
-                    break;
+                mDelegate.onMotionEvent(ev);
             }
         }
     }
@@ -285,10 +228,13 @@
         }
 
         float velocityYPxPerS = mVelocityTracker.getYVelocity();
+        float dY = Math.abs(mLastPos.y - mDownPos.y);
         if (mCanPlayTaskbarBgAlphaAnimation
                 && mMotionMoveCount >= NUM_MOTION_MOVE_THRESHOLD // Arbitrary value
                 && velocityYPxPerS != 0 // Ignore these
-                && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold) {
+                && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold
+                && dY != 0
+                && dY > mTouchSlop) {
             mTaskbarActivityContext.playTaskbarBackgroundAlphaAnimation();
             mCanPlayTaskbarBgAlphaAnimation = false;
         }
@@ -301,9 +247,6 @@
             mTransitionCallback.onActionEnd();
         }
         mHasPassedTaskbarNavThreshold = false;
-        mIsInBubbleBarArea = false;
-        mIsVerticalGestureOverBubbleBar = false;
-        mIsPassedBubbleBarSlop = false;
 
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
@@ -313,22 +256,6 @@
         mMotionMoveCount = 0;
     }
 
-    private boolean isInBubbleBarArea(float x) {
-        if (mTaskbarActivityContext == null || !mIsTransientTaskbar) {
-            return false;
-        }
-        BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers();
-        if (controllers == null) {
-            return false;
-        }
-        if (controllers.bubbleStashController.isStashed()) {
-            return controllers.bubbleStashedHandleViewController.containsX((int) x);
-        } else {
-            Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds();
-            return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right;
-        }
-    }
-
     /**
      * Listen for hover events for the stashed taskbar.
      *
@@ -363,7 +290,7 @@
             // start a single unstash timeout if hovering bottom edge under the hinted taskbar.
             if (!sUnstashHandler.hasMessagesOrCallbacks()) {
                 sUnstashHandler.postDelayed(() -> {
-                    mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+                    mTaskbarActivityContext.onSwipeToUnstashTaskbar(false);
                     mIsStashedTaskbarHovered = false;
                 }, HOVER_TASKBAR_UNSTASH_TIMEOUT);
             }
@@ -391,7 +318,7 @@
             startStashedTaskbarHover(/* isHovered = */ true);
         } else if (mBottomEdgeBounds.contains(x, y)) {
             // If hover screen's bottom edge not below the stashed taskbar, unstash it.
-            mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+            mTaskbarActivityContext.onSwipeToUnstashTaskbar(false);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 1bec970..f7f3157 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -62,6 +62,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
@@ -103,6 +104,9 @@
 
     private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate);
 
+    private final InvariantDeviceProfile.OnIDPChangeListener mOnIDPChangeListener =
+            modelPropertiesChanged -> updateHint();
+
     private TISBindHelper mTISBindHelper;
 
     private BgDrawable mBackground;
@@ -115,6 +119,8 @@
 
     private AnimatorPlaybackController mLauncherStartAnim = null;
 
+    private TextView mHintView;
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -167,12 +173,9 @@
             }
         });
 
-        TextView hint = findViewById(R.id.hint);
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this);
-        if (!dp.isGestureMode) {
-            hint.setText(R.string.allset_button_hint);
-        }
-        hint.setAccessibilityDelegate(new SkipButtonAccessibilityDelegate());
+        mHintView = findViewById(R.id.hint);
+        mHintView.setAccessibilityDelegate(new SkipButtonAccessibilityDelegate());
+        updateHint();
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
 
@@ -190,7 +193,21 @@
                         LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary),
                 getTheme());
 
-        startBackgroundAnimation(dp.isTablet);
+        startBackgroundAnimation(getDP().isTablet);
+        getIDP().addOnChangeListener(mOnIDPChangeListener);
+    }
+
+    private InvariantDeviceProfile getIDP() {
+        return LauncherAppState.getInstance(this).getInvariantDeviceProfile();
+    }
+
+    private DeviceProfile getDP() {
+        return getIDP().getDeviceProfile(this);
+    }
+
+    private void updateHint() {
+        mHintView.setText(
+                getDP().isGestureMode ? R.string.allset_hint : R.string.allset_button_hint);
     }
 
     private void runOnUiHelperThread(Runnable runnable) {
@@ -202,7 +219,7 @@
     }
 
     private void startBackgroundAnimation(boolean forTablet) {
-        if (!Utilities.ATLEAST_S || mVibrator == null) {
+        if (mVibrator == null) {
             return;
         }
         boolean supportsThud = mVibrator.areAllPrimitivesSupported(
@@ -311,6 +328,7 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
+        getIDP().removeOnChangeListener(mOnIDPChangeListener);
         mTISBindHelper.onDestroy();
         clearBinderOverride();
         if (mBackgroundAnimatorListener != null) {
diff --git a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java
index 742b0fc..7a86db3 100644
--- a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java
+++ b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep.interaction;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -175,11 +173,8 @@
 
     void setFakeTaskViewFillColor(@ColorInt int colorResId) {
         mFullTaskView.setBackgroundColor(colorResId);
-
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()){
-            mTopTaskView.getBackground().setTint(colorResId);
-            mBottomTaskView.getBackground().setTint(colorResId);
-        }
+        mTopTaskView.getBackground().setTint(colorResId);
+        mBottomTaskView.getBackground().setTint(colorResId);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 6757cd8..be7f8e5 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep.interaction;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
 
@@ -40,35 +39,29 @@
     BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) {
         super(fragment, tutorialType);
         // Set the Lottie animation colors specifically for the Back gesture
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            LottieAnimationColorUtils.updateToArgbColors(
-                    mAnimatedGestureDemonstration,
-                    Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack,
-                            ".surfaceBack", fragment.mRootView.mColorSurfaceBack,
-                            ".secondaryBack", fragment.mRootView.mColorSecondaryBack));
+        LottieAnimationColorUtils.updateToArgbColors(
+                mAnimatedGestureDemonstration,
+                Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack,
+                        ".surfaceBack", fragment.mRootView.mColorSurfaceBack,
+                        ".secondaryBack", fragment.mRootView.mColorSecondaryBack));
 
-            LottieAnimationColorUtils.updateToArgbColors(
-                    mCheckmarkAnimation,
-                    Map.of(".checkmark",
-                            Utilities.isDarkTheme(mContext)
-                                    ? fragment.mRootView.mColorOnSurfaceBack
-                                    : fragment.mRootView.mColorSecondaryBack,
-                            ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack));
-        }
+        LottieAnimationColorUtils.updateToArgbColors(
+                mCheckmarkAnimation,
+                Map.of(".checkmark",
+                        Utilities.isDarkTheme(mContext)
+                                ? fragment.mRootView.mColorOnSurfaceBack
+                                : fragment.mRootView.mColorSecondaryBack,
+                        ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack));
     }
 
     @Override
     public int getIntroductionTitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.back_gesture_tutorial_title
-                : R.string.back_gesture_intro_title;
+        return R.string.back_gesture_tutorial_title;
     }
 
     @Override
     public int getIntroductionSubtitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.back_gesture_tutorial_subtitle
-                : R.string.back_gesture_intro_subtitle;
+        return R.string.back_gesture_tutorial_subtitle;
     }
 
     @Override
@@ -85,9 +78,7 @@
     public int getSuccessFeedbackSubtitle() {
         return mTutorialFragment.isAtFinalStep()
                 ? R.string.back_gesture_feedback_complete_without_follow_up
-                : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                        ? R.string.back_gesture_feedback_complete_with_follow_up
-                        : R.string.back_gesture_feedback_complete_with_overview_follow_up;
+                : R.string.back_gesture_feedback_complete_with_follow_up;
     }
 
     @Override
@@ -128,20 +119,12 @@
 
     @LayoutRes
     int getMockAppTaskCurrentPageLayoutResId() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.layout.back_gesture_tutorial_background
-                : mTutorialFragment.isLargeScreen()
-                        ? R.layout.gesture_tutorial_tablet_mock_conversation
-                        : R.layout.gesture_tutorial_mock_conversation;
+        return R.layout.back_gesture_tutorial_background;
     }
 
     @LayoutRes
     int getMockAppTaskPreviousPageLayoutResId() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.layout.back_gesture_tutorial_background
-                : mTutorialFragment.isLargeScreen()
-                        ? R.layout.gesture_tutorial_tablet_mock_conversation_list
-                        : R.layout.gesture_tutorial_mock_conversation_list;
+        return R.layout.back_gesture_tutorial_background;
     }
 
     @Override
@@ -214,17 +197,13 @@
     }
 
     private void handleBackAttempt(BackGestureResult result) {
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            resetViewsForBackGesture();
-        }
+        resetViewsForBackGesture();
 
         switch (result) {
             case BACK_COMPLETED_FROM_LEFT:
             case BACK_COMPLETED_FROM_RIGHT:
                 mTutorialFragment.releaseFeedbackAnimation();
-                if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-                    mExitingAppView.setVisibility(View.GONE);
-                }
+                mExitingAppView.setVisibility(View.GONE);
                 updateFakeAppTaskViewLayout(getMockAppTaskPreviousPageLayoutResId());
                 showSuccessFeedback();
                 break;
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
index 1b12be8..700fbf8 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep.interaction;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Point;
@@ -211,10 +209,8 @@
                 }
             }
 
-            if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-                mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x,
-                        ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel());
-            }
+            mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x,
+                    ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel());
 
             // forward touch
             mEdgeBackPanel.onMotionEvent(ev);
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 36ea926..bc5cc15 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -37,10 +37,10 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.quickstep.TouchInteractionService.TISBinder;
 import com.android.quickstep.interaction.TutorialController.TutorialType;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.TISBindHelper;
 
 import java.util.ArrayList;
@@ -54,8 +54,6 @@
     static final String KEY_TUTORIAL_TYPE = "tutorial_type";
     static final String KEY_GESTURE_COMPLETE = "gesture_complete";
     static final String KEY_USE_TUTORIAL_MENU = "use_tutorial_menu";
-    public static final double SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95;
-    public static final double SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05;
 
     @Nullable private TutorialType[] mTutorialSteps;
     private GestureSandboxFragment mCurrentFragment;
@@ -80,9 +78,7 @@
         Bundle args = savedInstanceState == null ? getIntent().getExtras() : savedInstanceState;
 
         boolean gestureComplete = args != null && args.getBoolean(KEY_GESTURE_COMPLETE, false);
-        if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                && args != null
-                && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) {
+        if (args != null && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) {
             mTutorialSteps = null;
             TutorialType tutorialTypeOverride = (TutorialType) args.get(KEY_TUTORIAL_TYPE);
             mCurrentFragment = tutorialTypeOverride == null
@@ -102,9 +98,7 @@
                 .add(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
                 .commit();
 
-        if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            correctUserOrientation();
-        }
+        correctUserOrientation();
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
 
         initWindowInsets();
@@ -116,9 +110,7 @@
         super.onConfigurationChanged(newConfig);
 
         // Ensure the prompt to rotate the screen is updated
-        if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            correctUserOrientation();
-        }
+        correctUserOrientation();
     }
 
     private void initWindowInsets() {
@@ -170,10 +162,7 @@
                 getApplicationContext()).getDeviceProfile(this);
         if (deviceProfile.isTablet) {
             // The tutorial will work in either orientation if the height and width are similar
-            boolean isAspectRatioSquare =
-                    deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND
-                            && deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND;
-            boolean showRotationPrompt = !isAspectRatioSquare
+            boolean showRotationPrompt = !LayoutUtils.isAspectRatioSquare(deviceProfile.aspectRatio)
                     && getResources().getConfiguration().orientation
                     == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
index 1129e02..bf4eaf2 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep.interaction;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
-
 import android.graphics.PointF;
 
 import com.android.launcher3.R;
@@ -34,35 +32,29 @@
         super(fragment, tutorialType);
 
         // Set the Lottie animation colors specifically for the Home gesture
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            LottieAnimationColorUtils.updateToArgbColors(
-                    mAnimatedGestureDemonstration,
-                    Map.of(".onSurfaceHome", fragment.mRootView.mColorOnSurfaceHome,
-                            ".surfaceHome", fragment.mRootView.mColorSurfaceHome,
-                            ".secondaryHome", fragment.mRootView.mColorSecondaryHome));
+        LottieAnimationColorUtils.updateToArgbColors(
+                mAnimatedGestureDemonstration,
+                Map.of(".onSurfaceHome", fragment.mRootView.mColorOnSurfaceHome,
+                        ".surfaceHome", fragment.mRootView.mColorSurfaceHome,
+                        ".secondaryHome", fragment.mRootView.mColorSecondaryHome));
 
-            LottieAnimationColorUtils.updateToArgbColors(
-                    mCheckmarkAnimation,
-                    Map.of(".checkmark",
-                            Utilities.isDarkTheme(mContext)
-                                    ? fragment.mRootView.mColorOnSurfaceHome
-                                    : fragment.mRootView.mColorSecondaryHome,
-                            ".checkmarkBackground", fragment.mRootView.mColorSurfaceHome));
-        }
+        LottieAnimationColorUtils.updateToArgbColors(
+                mCheckmarkAnimation,
+                Map.of(".checkmark",
+                        Utilities.isDarkTheme(mContext)
+                                ? fragment.mRootView.mColorOnSurfaceHome
+                                : fragment.mRootView.mColorSecondaryHome,
+                        ".checkmarkBackground", fragment.mRootView.mColorSurfaceHome));
     }
 
     @Override
     public int getIntroductionTitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.home_gesture_tutorial_title
-                : R.string.home_gesture_intro_title;
+        return R.string.home_gesture_tutorial_title;
     }
 
     @Override
     public int getIntroductionSubtitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.home_gesture_tutorial_subtitle
-                : R.string.home_gesture_intro_subtitle;
+        return R.string.home_gesture_tutorial_subtitle;
     }
 
     @Override
@@ -72,9 +64,7 @@
 
     @Override
     public int getSuccessFeedbackTitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.home_gesture_tutorial_success
-                : R.string.gesture_tutorial_nice;
+        return R.string.home_gesture_tutorial_success;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
index a04dd44..e45f8d8 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
@@ -16,7 +16,6 @@
 package com.android.quickstep.interaction;
 
 import static com.android.app.animation.Interpolators.ACCELERATE;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -49,34 +48,28 @@
         super(fragment, tutorialType);
 
         // Set the Lottie animation colors specifically for the Overview gesture
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            LottieAnimationColorUtils.updateToArgbColors(
-                    mAnimatedGestureDemonstration,
-                    Map.of(".onSurfaceOverview", fragment.mRootView.mColorOnSurfaceOverview,
-                            ".surfaceOverview", fragment.mRootView.mColorSurfaceOverview,
-                            ".secondaryOverview", fragment.mRootView.mColorSecondaryOverview));
+        LottieAnimationColorUtils.updateToArgbColors(
+                mAnimatedGestureDemonstration,
+                Map.of(".onSurfaceOverview", fragment.mRootView.mColorOnSurfaceOverview,
+                        ".surfaceOverview", fragment.mRootView.mColorSurfaceOverview,
+                        ".secondaryOverview", fragment.mRootView.mColorSecondaryOverview));
 
-            LottieAnimationColorUtils.updateToArgbColors(
-                    mCheckmarkAnimation,
-                    Map.of(".checkmark",
-                            Utilities.isDarkTheme(mContext)
-                                    ? fragment.mRootView.mColorOnSurfaceOverview
-                                    : fragment.mRootView.mColorSecondaryOverview,
-                            ".checkmarkBackground", fragment.mRootView.mColorSurfaceOverview));
-        }
+        LottieAnimationColorUtils.updateToArgbColors(
+                mCheckmarkAnimation,
+                Map.of(".checkmark",
+                        Utilities.isDarkTheme(mContext)
+                                ? fragment.mRootView.mColorOnSurfaceOverview
+                                : fragment.mRootView.mColorSecondaryOverview,
+                        ".checkmarkBackground", fragment.mRootView.mColorSurfaceOverview));
     }
     @Override
     public int getIntroductionTitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.overview_gesture_tutorial_title
-                : R.string.overview_gesture_intro_title;
+        return R.string.overview_gesture_tutorial_title;
     }
 
     @Override
     public int getIntroductionSubtitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.overview_gesture_tutorial_subtitle
-                : R.string.overview_gesture_intro_subtitle;
+        return R.string.overview_gesture_tutorial_subtitle;
     }
 
     @Override
@@ -86,9 +79,7 @@
 
     @Override
     public int getSuccessFeedbackTitle() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.string.overview_gesture_tutorial_success
-                : R.string.gesture_tutorial_nice;
+        return R.string.overview_gesture_tutorial_success;
     }
 
     @Override
@@ -168,10 +159,7 @@
 
     @Override
     protected int getMockPreviousAppTaskThumbnailColor() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? mTutorialFragment.mRootView.mColorSurfaceContainer
-                : mContext.getResources().getColor(
-                        R.color.gesture_tutorial_fake_previous_task_view_color);
+        return mTutorialFragment.mRootView.mColorSurfaceContainer;
     }
 
     @Override
@@ -224,11 +212,8 @@
                     case OVERVIEW_GESTURE_COMPLETED:
                         setGestureCompleted();
                         mTutorialFragment.releaseFeedbackAnimation();
-                        animateTaskViewToOverview(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get());
+                        animateTaskViewToOverview(true);
                         onMotionPaused(true /*arbitrary value*/);
-                        if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-                            showSuccessFeedback();
-                        }
                         break;
                     case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
                     case HOME_OR_OVERVIEW_CANCELLED:
diff --git a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java
index affedb9..d733267 100644
--- a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java
+++ b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java
@@ -15,16 +15,12 @@
  */
 package com.android.quickstep.interaction;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
-
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.Insets;
-import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.WindowInsets;
 import android.widget.RelativeLayout;
 
@@ -39,10 +35,6 @@
 /** Root layout that TutorialFragment uses to intercept motion events. */
 public class RootSandboxLayout extends RelativeLayout {
 
-    private final Rect mTempStepIndicatorBounds = new Rect();
-    private final Rect mTempInclusionBounds = new Rect();
-    private final Rect mTempExclusionBounds = new Rect();
-
     @ColorInt final int mColorSurfaceContainer;
     @ColorInt final int mColorOnSurfaceHome;
     @ColorInt final int mColorSurfaceHome;
@@ -54,11 +46,6 @@
     @ColorInt final int mColorSurfaceOverview;
     @ColorInt final int mColorSecondaryOverview;
 
-    private View mFeedbackView;
-    private View mTutorialStepView;
-    private View mSkipButton;
-    private View mDoneButton;
-
     public RootSandboxLayout(Context context) {
         this(context, null);
     }
@@ -123,56 +110,4 @@
 
         return getHeight() + insets.top + insets.bottom;
     }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            return;
-        }
-        mFeedbackView = findViewById(R.id.gesture_tutorial_fragment_feedback_view);
-        mTutorialStepView =
-                mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step);
-        mSkipButton = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_close_button);
-        mDoneButton = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_action_button);
-
-        mFeedbackView.addOnLayoutChangeListener(
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                    if (mSkipButton.getVisibility() != VISIBLE
-                            && mDoneButton.getVisibility() != VISIBLE) {
-                        return;
-                    }
-                    // Either the skip or the done button is ever shown at once, never both.
-                    boolean showingSkipButton = mSkipButton.getVisibility() == VISIBLE;
-                    boolean isRTL = Utilities.isRtl(getContext().getResources());
-                    updateTutorialStepViewTranslation(
-                            showingSkipButton ? mSkipButton : mDoneButton,
-                            // Translate the step indicator away from whichever button is being
-                            // shown. The skip button in on the left in LTR or on the right in RTL.
-                            // The done button is on the right in LTR or left in RTL.
-                            (showingSkipButton && !isRTL) || (!showingSkipButton && isRTL));
-                });
-    }
-
-    private void updateTutorialStepViewTranslation(
-            @NonNull View anchorView, boolean translateToRight) {
-        mTempStepIndicatorBounds.set(
-                mTutorialStepView.getLeft(),
-                mTutorialStepView.getTop(),
-                mTutorialStepView.getRight(),
-                mTutorialStepView.getBottom());
-        mTempInclusionBounds.set(0, 0, mFeedbackView.getWidth(), mFeedbackView.getHeight());
-        mTempExclusionBounds.set(
-                anchorView.getLeft(),
-                anchorView.getTop(),
-                anchorView.getRight(),
-                anchorView.getBottom());
-
-        Utilities.translateOverlappingView(
-                mTutorialStepView,
-                mTempStepIndicatorBounds,
-                mTempInclusionBounds,
-                mTempExclusionBounds,
-                translateToRight ? Utilities.TRANSLATE_RIGHT : Utilities.TRANSLATE_LEFT);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index ad13efb..e462706 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -48,7 +48,6 @@
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.RecentsAnimationDeviceState;
@@ -127,9 +126,7 @@
     void resetTaskViews() {
         mFakeHotseatView.setVisibility(View.INVISIBLE);
         mFakeIconView.setVisibility(View.INVISIBLE);
-        if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            mFakeIconView.getBackground().setTint(getFakeTaskViewColor());
-        }
+        mFakeIconView.getBackground().setTint(getFakeTaskViewColor());
         if (mTutorialFragment.getActivity() != null) {
             int height = mTutorialFragment.getRootView().getFullscreenHeight();
             int width = mTutorialFragment.getRootView().getWidth();
@@ -138,9 +135,7 @@
         mFakeTaskViewRadius = 0;
         mFakeTaskView.invalidateOutline();
         mFakeTaskView.setVisibility(View.VISIBLE);
-        if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
-        }
+        mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
         mFakeTaskView.setAlpha(1);
         mFakePreviousTaskView.setVisibility(View.INVISIBLE);
         mFakePreviousTaskView.setAlpha(1);
@@ -390,12 +385,10 @@
                             false, /* isOpening */
                             mFakeIconView, mDp);
                     mFakeIconView.setAlpha(1);
-                    if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-                        int iconColor = ColorUtils.blendARGB(
-                                getFakeTaskViewColor(), getHotseatIconColor(), progress);
-                        mFakeIconView.getBackground().setTint(iconColor);
-                        mFakeTaskView.setBackgroundColor(iconColor);
-                    }
+                    int iconColor = ColorUtils.blendARGB(
+                            getFakeTaskViewColor(), getHotseatIconColor(), progress);
+                    mFakeIconView.getBackground().setTint(iconColor);
+                    mFakeTaskView.setBackgroundColor(iconColor);
                     mFakeTaskView.setAlpha(getWindowAlpha(progress));
                     mFakePreviousTaskView.setAlpha(getWindowAlpha(progress));
                 }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index f89888a..9510a05 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -19,10 +19,7 @@
 import static android.view.View.NO_ID;
 import static android.view.View.inflate;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
-
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
@@ -33,7 +30,6 @@
 import android.graphics.Matrix;
 import android.graphics.Outline;
 import android.graphics.Rect;
-import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.RippleDrawable;
 import android.util.Log;
@@ -52,12 +48,10 @@
 import androidx.annotation.ColorInt;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.LayoutRes;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 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;
@@ -153,8 +147,7 @@
         mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view);
         mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
-        mFakeTaskbarView = ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? null : rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view);
+        mFakeTaskbarView = null;
         mFakePreviousTaskView =
                 rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view);
         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
@@ -165,32 +158,30 @@
         mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot);
         mSkipTutorialDialog = createSkipTutorialDialog();
 
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration);
-            mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation);
-            mAnimatedGestureDemonstration = rootView.findViewById(
-                    R.id.gesture_demonstration_animations);
-            mExitingAppView = rootView.findViewById(R.id.exiting_app_back);
-            mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx;
-            mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx;
-            mExitingAppMargin = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.gesture_tutorial_back_gesture_exiting_app_margin);
-            mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext);
-            mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.gesture_tutorial_back_gesture_end_corner_radius);
-            mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener(
-                    this::createScalingMatrix);
+        mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration);
+        mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation);
+        mAnimatedGestureDemonstration = rootView.findViewById(
+                R.id.gesture_demonstration_animations);
+        mExitingAppView = rootView.findViewById(R.id.exiting_app_back);
+        mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx;
+        mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx;
+        mExitingAppMargin = mContext.getResources().getDimensionPixelSize(
+                R.dimen.gesture_tutorial_back_gesture_exiting_app_margin);
+        mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext);
+        mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize(
+                R.dimen.gesture_tutorial_back_gesture_end_corner_radius);
+        mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener(
+                this::createScalingMatrix);
 
-            mFeedbackTitleView.setText(getIntroductionTitle());
-            mFeedbackSubtitleView.setText(getIntroductionSubtitle());
-            mExitingAppView.setClipToOutline(true);
-            mExitingAppView.setOutlineProvider(new ViewOutlineProvider() {
-                @Override
-                public void getOutline(View view, Outline outline) {
-                    outline.setRoundRect(mExitingAppRect, mExitingAppRadius);
-                }
-            });
-        }
+        mFeedbackTitleView.setText(getIntroductionTitle());
+        mFeedbackSubtitleView.setText(getIntroductionSubtitle());
+        mExitingAppView.setClipToOutline(true);
+        mExitingAppView.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(mExitingAppRect, mExitingAppRadius);
+            }
+        });
 
         mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
                 AccessibilityEvent.TYPE_VIEW_FOCUSED);
@@ -261,19 +252,11 @@
 
     @LayoutRes
     protected int getMockHotseatResId() {
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            return mTutorialFragment.isLargeScreen()
-                    ? mTutorialFragment.isFoldable()
-                        ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat
-                        : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat
-                    : R.layout.redesigned_gesture_tutorial_mock_hotseat;
-        } else {
-            return mTutorialFragment.isLargeScreen()
-                    ? mTutorialFragment.isFoldable()
-                        ? R.layout.gesture_tutorial_foldable_mock_hotseat
-                        : R.layout.gesture_tutorial_tablet_mock_hotseat
-                    : R.layout.gesture_tutorial_mock_hotseat;
-        }
+        return mTutorialFragment.isLargeScreen()
+                ? mTutorialFragment.isFoldable()
+                    ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat
+                    : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat
+                : R.layout.redesigned_gesture_tutorial_mock_hotseat;
     }
 
     @LayoutRes
@@ -312,9 +295,7 @@
 
     @DrawableRes
     public int getMockAppIconResId() {
-        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                ? R.drawable.redesigned_hotseat_icon
-                : R.drawable.default_sandbox_app_icon;
+        return R.drawable.redesigned_hotseat_icon;
     }
 
     @DrawableRes
@@ -374,11 +355,7 @@
             mFeedbackView.setTranslationY(0);
             return;
         }
-        Animator gestureAnimation = mTutorialFragment.getGestureAnimation();
-        AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation();
-        if (gestureAnimation != null && edgeAnimation != null) {
-            playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true);
-        }
+        playFeedbackAnimation();
     }
 
     /**
@@ -442,12 +419,7 @@
         }
 
         mFeedbackTitleView.setText(titleResId);
-        mFeedbackSubtitleView.setText(
-                ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() || spokenSubtitleResId == NO_ID
-                        ? mContext.getText(subtitleResId)
-                        : Utilities.wrapForTts(
-                                mContext.getText(subtitleResId),
-                                mContext.getString(spokenSubtitleResId)));
+        mFeedbackSubtitleView.setText(subtitleResId);
         if (isGestureSuccessful) {
             if (mTutorialFragment.isAtFinalStep()) {
                 showActionButton();
@@ -458,27 +430,16 @@
                 mFakeTaskViewCallback = null;
             }
 
-            if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-                showSuccessPage();
-            }
+            showSuccessPage();
         }
         mGestureCompleted = isGestureSuccessful;
-
-        Animator gestureAnimation = mTutorialFragment.getGestureAnimation();
-        AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation();
-        if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) {
-            playFeedbackAnimation(
-                    gestureAnimation,
-                    edgeAnimation,
-                    mShowFeedbackRunnable,
-                    useGestureAnimationDelay);
-            return;
+        if (!isGestureSuccessful) {
+            playFeedbackAnimation();
         } else {
             mTutorialFragment.releaseFeedbackAnimation();
+            mFeedbackViewCallback = mShowFeedbackRunnable;
+            mFeedbackView.post(mFeedbackViewCallback);
         }
-        mFeedbackViewCallback = mShowFeedbackRunnable;
-
-        mFeedbackView.post(mFeedbackViewCallback);
     }
 
     private void showSuccessPage() {
@@ -517,79 +478,17 @@
         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
     }
 
-    private void playFeedbackAnimation(
-            @NonNull Animator gestureAnimation,
-            @NonNull AnimatedVectorDrawable edgeAnimation,
-            @NonNull Runnable onStartRunnable,
-            boolean useGestureAnimationDelay) {
-
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            mFeedbackView.setVisibility(View.VISIBLE);
-            mAnimatedGestureDemonstration.setVisibility(View.VISIBLE);
-            mFullGestureDemonstration.setVisibility(View.VISIBLE);
-            mAnimatedGestureDemonstration.playAnimation();
-            return;
-        }
-
-        if (gestureAnimation.isRunning()) {
-            gestureAnimation.cancel();
-        }
-        if (edgeAnimation.isRunning()) {
-            edgeAnimation.reset();
-        }
-        gestureAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                super.onAnimationStart(animation);
-
-                mEdgeGestureVideoView.setVisibility(GONE);
-                if (edgeAnimation.isRunning()) {
-                    edgeAnimation.stop();
-                }
-
-                if (!useGestureAnimationDelay) {
-                    onStartRunnable.run();
-                }
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                super.onAnimationEnd(animation);
-
-                mEdgeGestureVideoView.setVisibility(View.VISIBLE);
-                edgeAnimation.start();
-
-                gestureAnimation.removeListener(this);
-            }
-        });
-
-        cancelQueuedGestureAnimation();
-        if (useGestureAnimationDelay) {
-            mFeedbackViewCallback = onStartRunnable;
-            mFakeTaskViewCallback = gestureAnimation::start;
-
-            mFeedbackView.post(mFeedbackViewCallback);
-            mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS);
-        } else {
-            gestureAnimation.start();
-        }
+    private void playFeedbackAnimation() {
+        mFeedbackView.setVisibility(View.VISIBLE);
+        mAnimatedGestureDemonstration.setVisibility(View.VISIBLE);
+        mFullGestureDemonstration.setVisibility(View.VISIBLE);
+        mAnimatedGestureDemonstration.playAnimation();
     }
 
     void setRippleHotspot(float x, float y) {
         mRippleDrawable.setHotspot(x, y);
     }
 
-    void showRippleEffect(@Nullable Runnable onCompleteRunnable) {
-        mRippleDrawable.setState(
-                new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled});
-        mRippleView.postDelayed(() -> {
-            mRippleDrawable.setState(new int[] {});
-            if (onCompleteRunnable != null) {
-                onCompleteRunnable.run();
-            }
-        }, RIPPLE_VISIBLE_MS);
-    }
-
     void onActionButtonClicked(View button) {
         mTutorialFragment.continueTutorial();
     }
@@ -601,24 +500,19 @@
         updateDrawables();
         updateLayout();
 
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance());
-            mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance());
-            mDoneButton.getBackground().setTint(getDoneButtonColor());
-            mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep()
-                    ? R.raw.checkmark_animation_end
-                    : R.raw.checkmark_animation_in_progress);
-            if (!isGestureCompleted()) {
-                mCheckmarkAnimation.setVisibility(GONE);
-                startGestureAnimation();
-                if (mTutorialType == TutorialType.BACK_NAVIGATION) {
-                    resetViewsForBackGesture();
-                }
-
+        mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance());
+        mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance());
+        mDoneButton.getBackground().setTint(getDoneButtonColor());
+        mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep()
+                ? R.raw.checkmark_animation_end
+                : R.raw.checkmark_animation_in_progress);
+        if (!isGestureCompleted()) {
+            mCheckmarkAnimation.setVisibility(GONE);
+            startGestureAnimation();
+            if (mTutorialType == TutorialType.BACK_NAVIGATION) {
+                resetViewsForBackGesture();
             }
-        } else {
-            hideFeedback();
-            hideActionButton();
+
         }
 
         mGestureCompleted = false;
@@ -654,13 +548,6 @@
                 : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark);
     }
 
-    void hideActionButton() {
-        mSkipButton.setVisibility(View.VISIBLE);
-        // Invisible to maintain the layout.
-        mDoneButton.setVisibility(View.INVISIBLE);
-        mDoneButton.setOnClickListener(null);
-    }
-
     void showActionButton() {
         mSkipButton.setVisibility(GONE);
         mDoneButton.setVisibility(View.VISIBLE);
@@ -711,10 +598,8 @@
     }
 
     private void updateSubtext() {
-        if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            mTutorialStepView.setTutorialProgress(
-                    mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps());
-        }
+        mTutorialStepView.setTutorialProgress(
+                mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps());
     }
 
     private void updateHotseatChildViewColor(@Nullable View child) {
@@ -724,33 +609,25 @@
 
     private void updateDrawables() {
         if (mContext != null) {
-            mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
-                    mContext, getMockWallpaperResId()));
+            mTutorialFragment.getRootView()
+                    .setBackground(mContext.getDrawable(getMockWallpaperResId()));
             mTutorialFragment.updateFeedbackAnimation();
-            mFakeLauncherView.setBackgroundColor(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                    ? getFakeLauncherColor()
-                    : mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color));
+            mFakeLauncherView.setBackgroundColor(getFakeLauncherColor());
             updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId());
             mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1);
             mFakeTaskView.animate().alpha(1).setListener(
                     AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
             mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor());
-            mFakeIconView.setBackground(AppCompatResources.getDrawable(
-                    mContext, getMockAppIconResId()));
-
-            if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-                mExitingAppView.setBackgroundColor(getExitingAppColor());
-                mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
-                updateHotseatChildViewColor(mHotseatIconView);
-                updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2));
-                updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3));
-                updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4));
-                updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5));
-                updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6));
-                updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar));
-            } else {
-                updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId());
-            }
+            mFakeIconView.setBackground(mContext.getDrawable(getMockAppIconResId()));
+            mExitingAppView.setBackgroundColor(getExitingAppColor());
+            mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
+            updateHotseatChildViewColor(mHotseatIconView);
+            updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2));
+            updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3));
+            updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4));
+            updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5));
+            updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6));
+            updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar));
         }
     }
 
@@ -782,7 +659,6 @@
                 (RelativeLayout.LayoutParams) mFakeHotseatView.getLayoutParams();
         if (!mTutorialFragment.isLargeScreen()) {
             DeviceProfile dp = mTutorialFragment.getDeviceProfile();
-            dp.updateIsSeascape(mContext);
 
             hotseatLayoutParams.addRule(dp.isLandscape
                     ? (dp.isSeascape()
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 0fafb94..2ff2c83 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -17,7 +17,6 @@
 
 import static android.view.View.NO_ID;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_GESTURE_COMPLETE;
 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_TUTORIAL_TYPE;
 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_USE_TUTORIAL_MENU;
@@ -215,9 +214,7 @@
         super.onCreateView(inflater, container, savedInstanceState);
 
         mRootView = (RootSandboxLayout) inflater.inflate(
-                ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                        ? R.layout.redesigned_gesture_tutorial_fragment
-                        : R.layout.gesture_tutorial_fragment,
+                R.layout.redesigned_gesture_tutorial_fragment,
                 container,
                 false);
 
@@ -383,10 +380,7 @@
         if (mTutorialController != null && !isGestureComplete()) {
             mTutorialController.hideFeedback();
         }
-
-        if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
-            mTutorialController.pauseAndHideLottieAnimation();
-        }
+        mTutorialController.pauseAndHideLottieAnimation();
 
         // Note: Using logical-or to ensure both functions get called.
         return mEdgeBackGestureHandler.onTouch(view, motionEvent)
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 5ac04da..dd721e1 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -39,9 +39,11 @@
 import android.util.Log;
 import android.util.Xml;
 
-import com.android.launcher3.AutoInstallsLayout;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
+import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
@@ -73,7 +75,6 @@
             new MainThreadInitializedObject<>(SettingsChangeLogger::new);
 
     private static final String TAG = "SettingsChangeLogger";
-    private static final String ROOT_TAG = "androidx.preference.PreferenceScreen";
     private static final String BOOLEAN_PREF = "SwitchPreference";
 
     private final Context mContext;
@@ -84,20 +85,26 @@
     private StatsLogManager.LauncherEvent mNotificationDotsEvent;
     private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent;
 
-    private SettingsChangeLogger(Context context) {
+    SettingsChangeLogger(@ApplicationContext Context context) {
+        this(context, StatsLogManager.newInstance(context));
+    }
+
+    @VisibleForTesting
+    SettingsChangeLogger(Context context, StatsLogManager statsLogManager) {
         mContext = context;
-        mStatsLogManager = StatsLogManager.newInstance(mContext);
+        mStatsLogManager = statsLogManager;
         mLoggablePrefs = loadPrefKeys(context);
+
         DisplayController.INSTANCE.get(context).addChangeListener(this);
         mNavMode = DisplayController.getNavigationMode(context);
 
         getPrefs(context).registerOnSharedPreferenceChangeListener(this);
         getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
 
-        SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context);
-        mSettingsCache.register(NOTIFICATION_BADGING_URI,
+        SettingsCache settingsCache = SettingsCache.INSTANCE.get(context);
+        settingsCache.register(NOTIFICATION_BADGING_URI,
                 this::onNotificationDotsChanged);
-        onNotificationDotsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI));
+        onNotificationDotsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
     }
 
     private static ArrayMap<String, LoggablePref> loadPrefKeys(Context context) {
@@ -105,7 +112,13 @@
         ArrayMap<String, LoggablePref> result = new ArrayMap<>();
 
         try {
-            AutoInstallsLayout.beginDocument(parser, ROOT_TAG);
+            // Move cursor to first tag because it could be
+            // androidx.preference.PreferenceScreen or PreferenceScreen
+            int eventType = parser.getEventType();
+            while (eventType != XmlPullParser.START_TAG
+                    && eventType != XmlPullParser.END_DOCUMENT) {
+                eventType = parser.next();
+            }
             final int depth = parser.getDepth();
             int type;
             while (((type = parser.next()) != XmlPullParser.END_TAG
@@ -189,13 +202,19 @@
                 prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff));
     }
 
+    @VisibleForTesting
+    ArrayMap<String, LoggablePref> getLoggingPrefs() {
+        return mLoggablePrefs;
+    }
+
     @Override
     public void close() {
         getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
         getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
     }
 
-    private static class LoggablePref {
+    @VisibleForTesting
+    static class LoggablePref {
         public boolean defaultValue;
         public int eventIdOn;
         public int eventIdOff;
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/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index ec04cb7..88ef0a8 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -37,6 +37,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.component1
 import androidx.core.util.component2
+import androidx.core.view.updateLayoutParams
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAnimUtils
@@ -242,7 +243,30 @@
         lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
     }
 
-    override fun getDwbLayoutTranslations(
+    override fun updateDwbBannerLayout(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        isGroupedTaskView: Boolean,
+        deviceProfile: DeviceProfile,
+        snapshotViewWidth: Int,
+        snapshotViewHeight: Int,
+        banner: View
+    ) {
+        banner.pivotX = 0f
+        banner.pivotY = 0f
+        banner.rotation = degreesRotated
+        banner.updateLayoutParams<FrameLayout.LayoutParams> {
+            gravity = Gravity.TOP or if (banner.isLayoutRtl) Gravity.END else Gravity.START
+            width =
+                if (isGroupedTaskView) {
+                    snapshotViewHeight
+                } else {
+                    taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
+                }
+        }
+    }
+
+    override fun getDwbBannerTranslations(
         taskViewWidth: Int,
         taskViewHeight: Int,
         splitBounds: SplitBounds?,
@@ -252,39 +276,25 @@
         banner: View
     ): Pair<Float, Float> {
         val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
-        val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
         val translationX = banner.height.toFloat()
-
-        val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
-        bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START
-        banner.pivotX = 0f
-        banner.pivotY = 0f
-        banner.rotation = degreesRotated
-
-        if (splitBounds == null) {
-            // Single, fullscreen case
-            bannerParams.width = taskViewHeight - snapshotParams.topMargin
-            return Pair(translationX, snapshotParams.topMargin.toFloat())
-        }
-
-        // Set correct width and translations
         val translationY: Float
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].measuredHeight
+        if (splitBounds == null) {
             translationY = snapshotParams.topMargin.toFloat()
         } else {
-            bannerParams.width = thumbnailViews[1].measuredHeight
-            val topLeftTaskPlusDividerPercent =
-                if (splitBounds.appsStackedVertically) {
-                    splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
-                } else {
-                    splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
-                }
-            translationY =
-                snapshotParams.topMargin +
-                    (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
+            if (desiredTaskId == splitBounds.leftTopTaskId) {
+                translationY = snapshotParams.topMargin.toFloat()
+            } else {
+                val topLeftTaskPlusDividerPercent =
+                    if (splitBounds.appsStackedVertically) {
+                        splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
+                    } else {
+                        splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
+                    }
+                translationY =
+                    snapshotParams.topMargin +
+                        (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
+            }
         }
-
         return Pair(translationX, translationY)
     }
 
@@ -300,6 +310,7 @@
         if (isRtl) displacement < 0 else displacement > 0
 
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1
+
     /* -------------------- */
 
     override fun getChildBounds(
@@ -442,6 +453,10 @@
         }
     }
 
+    /**
+     * @param inSplitSelection Whether user currently has a task from this task group staged for
+     * split screen. Currently this state is not reachable in fake landscape.
+     */
     override fun measureGroupedTaskViewThumbnailBounds(
         primarySnapshot: View,
         secondarySnapshot: View,
@@ -449,7 +464,8 @@
         parentHeight: Int,
         splitBoundsConfig: SplitBounds,
         dp: DeviceProfile,
-        isRtl: Boolean
+        isRtl: Boolean,
+        inSplitSelection: Boolean
     ) {
         val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
         val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
@@ -558,6 +574,10 @@
         iconAppChipView.setRotation(degreesRotated)
     }
 
+    /**
+     * @param inSplitSelection Whether user currently has a task from this task group staged for
+     * split screen. Currently this state is not reachable in fake landscape.
+     */
     override fun setSplitIconParams(
         primaryIconView: View,
         secondaryIconView: View,
@@ -568,7 +588,8 @@
         groupedTaskViewWidth: Int,
         isRtl: Boolean,
         deviceProfile: DeviceProfile,
-        splitConfig: SplitBounds
+        splitConfig: SplitBounds,
+        inSplitSelection: Boolean
     ) {
         val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx
         val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index eeacee1..c0b697d 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -243,54 +243,54 @@
     }
 
     @Override
-    public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
-            int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
-            View[] thumbnailViews, int desiredTaskId, View banner) {
-        float translationX = 0;
-        float translationY = 0;
+    public void updateDwbBannerLayout(int taskViewWidth, int taskViewHeight,
+            boolean isGroupedTaskView, @NonNull DeviceProfile deviceProfile,
+            int snapshotViewWidth, int snapshotViewHeight, @NonNull View banner) {
         FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
         banner.setPivotX(0);
         banner.setPivotY(0);
         banner.setRotation(getDegreesRotated());
-        if (splitBounds == null) {
-            // Single, fullscreen case
+        if (isGroupedTaskView) {
+            bannerParams.gravity =
+                    BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
+            bannerParams.width = snapshotViewWidth;
+        } else {
             bannerParams.width = MATCH_PARENT;
             bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL;
-            return new Pair<>(translationX, translationY);
         }
+        banner.setLayoutParams(bannerParams);
+    }
 
-        bannerParams.gravity =
-                BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
-
-        // Set correct width
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].getMeasuredWidth();
-        } else {
-            bannerParams.width = thumbnailViews[1].getMeasuredWidth();
-        }
-
-        // Set translations
-        if (deviceProfile.isLeftRightSplit) {
-            if (desiredTaskId == splitBounds.rightBottomTaskId) {
-                float leftTopTaskPercent = splitBounds.appsStackedVertically
-                        ? splitBounds.topTaskPercent
-                        : splitBounds.leftTaskPercent;
-                float dividerThicknessPercent = splitBounds.appsStackedVertically
-                        ? splitBounds.dividerHeightPercent
-                        : splitBounds.dividerWidthPercent;
-                translationX = ((taskViewWidth * leftTopTaskPercent)
-                        + (taskViewWidth * dividerThicknessPercent));
-            }
-        } else {
-            if (desiredTaskId == splitBounds.leftTopTaskId) {
-                FrameLayout.LayoutParams snapshotParams =
-                        (FrameLayout.LayoutParams) thumbnailViews[0]
-                                .getLayoutParams();
-                float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
-                        ? (1f - splitBounds.topTaskPercent)
-                        : (1f - splitBounds.leftTaskPercent);
-                translationY = -((taskViewHeight - snapshotParams.topMargin)
-                        * bottomRightTaskPlusDividerPercent);
+    @NonNull
+    @Override
+    public Pair<Float, Float> getDwbBannerTranslations(int taskViewWidth,
+            int taskViewHeight, SplitBounds splitBounds, @NonNull DeviceProfile deviceProfile,
+            @NonNull View[] thumbnailViews, int desiredTaskId, @NonNull View banner) {
+        float translationX = 0;
+        float translationY = 0;
+        if (splitBounds != null) {
+            if (deviceProfile.isLeftRightSplit) {
+                if (desiredTaskId == splitBounds.rightBottomTaskId) {
+                    float leftTopTaskPercent = splitBounds.appsStackedVertically
+                            ? splitBounds.topTaskPercent
+                            : splitBounds.leftTaskPercent;
+                    float dividerThicknessPercent = splitBounds.appsStackedVertically
+                            ? splitBounds.dividerHeightPercent
+                            : splitBounds.dividerWidthPercent;
+                    translationX = ((taskViewWidth * leftTopTaskPercent)
+                            + (taskViewWidth * dividerThicknessPercent));
+                }
+            } else {
+                if (desiredTaskId == splitBounds.leftTopTaskId) {
+                    FrameLayout.LayoutParams snapshotParams =
+                            (FrameLayout.LayoutParams) thumbnailViews[0]
+                                    .getLayoutParams();
+                    float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
+                            ? (1f - splitBounds.topTaskPercent)
+                            : (1f - splitBounds.leftTaskPercent);
+                    translationY = -((taskViewHeight - snapshotParams.topMargin)
+                            * bottomRightTaskPlusDividerPercent);
+                }
             }
         }
         return new Pair<>(translationX, translationY);
@@ -530,49 +530,63 @@
         }
     }
 
+    /**
+     * @param inSplitSelection Whether user currently has a task from this task group staged for
+     *                         split screen. If true, we have custom translations/scaling in place
+     *                         for the remaining snapshot, so we'll skip setting translation/scale
+     *                         here.
+     */
     @Override
     public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
             int parentWidth, int parentHeight, SplitBounds splitBoundsConfig,
-            DeviceProfile dp, boolean isRtl) {
+            DeviceProfile dp, boolean isRtl, boolean inSplitSelection) {
         int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
+
+        FrameLayout.LayoutParams primaryParams =
+                (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
+        FrameLayout.LayoutParams secondaryParams =
+                (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
+
+        // Reset margins that aren't used in this method, but are used in other
+        // `RecentsPagedOrientationHandler` variants.
+        secondaryParams.topMargin = 0;
+        primaryParams.topMargin = spaceAboveSnapshot;
+
         int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
         float dividerScale = splitBoundsConfig.appsStackedVertically
                 ? splitBoundsConfig.dividerHeightPercent
                 : splitBoundsConfig.dividerWidthPercent;
         Pair<Point, Point> taskViewSizes =
                 getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
-        if (dp.isLeftRightSplit) {
-            int scaledDividerBar = Math.round(parentWidth * dividerScale);
-            if (isRtl) {
-                int translationX = taskViewSizes.second.x + scaledDividerBar;
-                primarySnapshot.setTranslationX(-translationX);
-                secondarySnapshot.setTranslationX(0);
+        if (!inSplitSelection) {
+            // Reset translations that aren't used in this method, but are used in other
+            // `RecentsPagedOrientationHandler` variants.
+            primarySnapshot.setTranslationY(0);
+
+            if (dp.isLeftRightSplit) {
+                int scaledDividerBar = Math.round(parentWidth * dividerScale);
+                if (isRtl) {
+                    int translationX = taskViewSizes.second.x + scaledDividerBar;
+                    primarySnapshot.setTranslationX(-translationX);
+                    secondarySnapshot.setTranslationX(0);
+                } else {
+                    int translationX = taskViewSizes.first.x + scaledDividerBar;
+                    secondarySnapshot.setTranslationX(translationX);
+                    primarySnapshot.setTranslationX(0);
+                }
+                secondarySnapshot.setTranslationY(spaceAboveSnapshot);
             } else {
-                int translationX = taskViewSizes.first.x + scaledDividerBar;
-                secondarySnapshot.setTranslationX(translationX);
+                float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
+                float translationY =
+                        taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight;
+                secondarySnapshot.setTranslationY(translationY);
+
+                // Reset unused translations.
+                secondarySnapshot.setTranslationX(0);
                 primarySnapshot.setTranslationX(0);
             }
-            secondarySnapshot.setTranslationY(spaceAboveSnapshot);
-
-            // Reset unused translations
-            primarySnapshot.setTranslationY(0);
-        } else {
-            float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
-            float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight;
-            secondarySnapshot.setTranslationY(translationY);
-
-            FrameLayout.LayoutParams primaryParams =
-                    (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
-            FrameLayout.LayoutParams secondaryParams =
-                    (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
-            secondaryParams.topMargin = 0;
-            primaryParams.topMargin = spaceAboveSnapshot;
-
-            // Reset unused translations
-            primarySnapshot.setTranslationY(0);
-            secondarySnapshot.setTranslationX(0);
-            primarySnapshot.setTranslationX(0);
         }
+
         primarySnapshot.measure(
                 View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
                 View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY));
@@ -580,10 +594,6 @@
                 View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
                 View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y,
                         View.MeasureSpec.EXACTLY));
-        primarySnapshot.setScaleX(1);
-        secondarySnapshot.setScaleX(1);
-        primarySnapshot.setScaleY(1);
-        secondarySnapshot.setScaleY(1);
     }
 
     @Override
@@ -660,11 +670,16 @@
         iconAppChipView.setRotation(getDegreesRotated());
     }
 
+    /**
+     * @param inSplitSelection Whether user currently has a task from this task group staged for
+     *                         split screen. If true, we have custom translations in place for the
+     *                         remaining icon, so we'll skip setting translations here.
+     */
     @Override
     public void setSplitIconParams(View primaryIconView, View secondaryIconView,
             int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
             int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
-            DeviceProfile deviceProfile, SplitBounds splitConfig) {
+            DeviceProfile deviceProfile, SplitBounds splitConfig, boolean inSplitSelection) {
         FrameLayout.LayoutParams primaryIconParams =
                 (FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
         FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu()
@@ -678,20 +693,23 @@
             secondaryIconParams.gravity = TOP | START;
             secondaryIconParams.topMargin = primaryIconParams.topMargin;
             secondaryIconParams.setMarginStart(primaryIconParams.getMarginStart());
-            if (deviceProfile.isLeftRightSplit) {
-                if (isRtl) {
-                    int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth;
-                    primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth);
+            if (!inSplitSelection) {
+                if (deviceProfile.isLeftRightSplit) {
+                    if (isRtl) {
+                        int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth;
+                        primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth);
+                    } else {
+                        secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth);
+                    }
                 } else {
-                    secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth);
+                    primaryAppChipView.setSplitTranslationX(0);
+                    secondaryAppChipView.setSplitTranslationX(0);
+                    int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
+                            splitConfig.visualDividerBounds.height());
+                    secondaryAppChipView.setSplitTranslationY(
+                            primarySnapshotHeight + (deviceProfile.isTablet ? 0
+                                    : dividerThickness));
                 }
-            } else {
-                primaryAppChipView.setSplitTranslationX(0);
-                secondaryAppChipView.setSplitTranslationX(0);
-                int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
-                        splitConfig.visualDividerBounds.height());
-                secondaryAppChipView.setSplitTranslationY(
-                        primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness));
             }
         } else if (deviceProfile.isLeftRightSplit) {
             // We calculate the "midpoint" of the thumbnail area, and place the icons there.
@@ -714,46 +732,53 @@
             if (deviceProfile.isSeascape()) {
                 primaryIconParams.gravity = TOP | (isRtl ? END : START);
                 secondaryIconParams.gravity = TOP | (isRtl ? END : START);
-                if (splitConfig.initiatedFromSeascape) {
-                    // if the split was initiated from seascape,
-                    // the task on the right (secondary) is slightly larger
-                    primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight);
-                    secondaryIconView.setTranslationX(bottomToMidpointOffset);
-                } else {
-                    // if not,
-                    // the task on the left (primary) is slightly larger
-                    primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset
-                            - taskIconHeight);
-                    secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset);
+                if (!inSplitSelection) {
+                    if (splitConfig.initiatedFromSeascape) {
+                        // if the split was initiated from seascape,
+                        // the task on the right (secondary) is slightly larger
+                        primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight);
+                        secondaryIconView.setTranslationX(bottomToMidpointOffset);
+                    } else {
+                        // if not,
+                        // the task on the left (primary) is slightly larger
+                        primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset
+                                - taskIconHeight);
+                        secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset);
+                    }
                 }
             } else {
                 primaryIconParams.gravity = TOP | (isRtl ? START : END);
                 secondaryIconParams.gravity = TOP | (isRtl ? START : END);
-                if (!splitConfig.initiatedFromSeascape) {
-                    // if the split was initiated from landscape,
-                    // the task on the left (primary) is slightly larger
-                    primaryIconView.setTranslationX(-bottomToMidpointOffset);
-                    secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight);
-                } else {
-                    // if not,
-                    // the task on the right (secondary) is slightly larger
-                    primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset);
-                    secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset
-                            + taskIconHeight);
+                if (!inSplitSelection) {
+                    if (!splitConfig.initiatedFromSeascape) {
+                        // if the split was initiated from landscape,
+                        // the task on the left (primary) is slightly larger
+                        primaryIconView.setTranslationX(-bottomToMidpointOffset);
+                        secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight);
+                    } else {
+                        // if not,
+                        // the task on the right (secondary) is slightly larger
+                        primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset);
+                        secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset
+                                + taskIconHeight);
+                    }
                 }
             }
         } else {
             primaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
-            // shifts icon half a width left (height is used here since icons are square)
-            primaryIconView.setTranslationX(-(taskIconHeight / 2f));
             secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
-            secondaryIconView.setTranslationX(taskIconHeight / 2f);
+            if (!inSplitSelection) {
+                // shifts icon half a width left (height is used here since icons are square)
+                primaryIconView.setTranslationX(-(taskIconHeight / 2f));
+                secondaryIconView.setTranslationX(taskIconHeight / 2f);
+            }
         }
-        if (!enableOverviewIconMenu()) {
+        if (!enableOverviewIconMenu() && !inSplitSelection) {
             primaryIconView.setTranslationY(0);
             secondaryIconView.setTranslationY(0);
         }
 
+
         primaryIconView.setLayoutParams(primaryIconParams);
         secondaryIconView.setLayoutParams(secondaryIconParams);
     }
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
index df4b030..b8d0412 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
@@ -184,7 +184,8 @@
         parentHeight: Int,
         splitBoundsConfig: SplitConfigurationOptions.SplitBounds,
         dp: DeviceProfile,
-        isRtl: Boolean
+        isRtl: Boolean,
+        inSplitSelection: Boolean
     )
 
     /**
@@ -199,6 +200,7 @@
         parentWidth: Int,
         parentHeight: Int
     ): Pair<Point, Point>
+
     // Overview TaskMenuView methods
     /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
     fun setTaskIconParams(
@@ -234,7 +236,8 @@
         groupedTaskViewWidth: Int,
         isRtl: Boolean,
         deviceProfile: DeviceProfile,
-        splitConfig: SplitConfigurationOptions.SplitBounds
+        splitConfig: SplitConfigurationOptions.SplitBounds,
+        inSplitSelection: Boolean
     )
 
     /*
@@ -294,13 +297,24 @@
         deviceProfile: DeviceProfile
     )
 
+    /** Layout a Digital Wellbeing Banner on its parent. TaskView. */
+    fun updateDwbBannerLayout(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        isGroupedTaskView: Boolean,
+        deviceProfile: DeviceProfile,
+        snapshotViewWidth: Int,
+        snapshotViewHeight: Int,
+        banner: View
+    )
+
     /**
-     * Calculates the position where a Digital Wellbeing Banner should be placed on its parent
+     * Calculates the translations where a Digital Wellbeing Banner should be apply on its parent
      * TaskView.
      *
      * @return A Pair of Floats representing the proper x and y translations.
      */
-    fun getDwbLayoutTranslations(
+    fun getDwbBannerTranslations(
         taskViewWidth: Int,
         taskViewHeight: Int,
         splitBounds: SplitConfigurationOptions.SplitBounds?,
@@ -309,6 +323,7 @@
         desiredTaskId: Int,
         banner: View
     ): Pair<Float, Float>
+
     // The following are only used by TaskViewTouchHandler.
 
     /** @return Either VERTICAL or HORIZONTAL. */
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index 333359f..bc91911 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -28,6 +28,7 @@
 import android.widget.FrameLayout
 import androidx.core.util.component1
 import androidx.core.util.component2
+import androidx.core.view.updateLayoutParams
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.Flags
 import com.android.launcher3.R
@@ -125,7 +126,30 @@
         }
     }
 
-    override fun getDwbLayoutTranslations(
+    override fun updateDwbBannerLayout(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        isGroupedTaskView: Boolean,
+        deviceProfile: DeviceProfile,
+        snapshotViewWidth: Int,
+        snapshotViewHeight: Int,
+        banner: View
+    ) {
+        banner.pivotX = 0f
+        banner.pivotY = 0f
+        banner.rotation = degreesRotated
+        banner.updateLayoutParams<FrameLayout.LayoutParams> {
+            gravity = Gravity.BOTTOM or if (banner.isLayoutRtl) Gravity.END else Gravity.START
+            width =
+                if (isGroupedTaskView) {
+                    snapshotViewHeight
+                } else {
+                    taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
+                }
+        }
+    }
+
+    override fun getDwbBannerTranslations(
         taskViewWidth: Int,
         taskViewHeight: Int,
         splitBounds: SplitBounds?,
@@ -135,39 +159,26 @@
         banner: View
     ): Pair<Float, Float> {
         val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
-        val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
-
-        val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
-        bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START
-        banner.pivotX = 0f
-        banner.pivotY = 0f
-        banner.rotation = degreesRotated
-
         val translationX: Float = (taskViewWidth - banner.height).toFloat()
-        if (splitBounds == null) {
-            // Single, fullscreen case
-            bannerParams.width = taskViewHeight - snapshotParams.topMargin
-            return Pair(translationX, banner.height.toFloat())
-        }
-
-        // Set correct width and translations
         val translationY: Float
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].measuredHeight
-            val bottomRightTaskPlusDividerPercent =
-                if (splitBounds.appsStackedVertically) {
-                    1f - splitBounds.topTaskPercent
-                } else {
-                    1f - splitBounds.leftTaskPercent
-                }
-            translationY =
-                banner.height -
-                    (taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent
-        } else {
-            bannerParams.width = thumbnailViews[1].measuredHeight
+        if (splitBounds == null) {
             translationY = banner.height.toFloat()
+        } else {
+            if (desiredTaskId == splitBounds.leftTopTaskId) {
+                val bottomRightTaskPlusDividerPercent =
+                    if (splitBounds.appsStackedVertically) {
+                        1f - splitBounds.topTaskPercent
+                    } else {
+                        1f - splitBounds.leftTaskPercent
+                    }
+                translationY =
+                    banner.height -
+                        (taskViewHeight - snapshotParams.topMargin) *
+                            bottomRightTaskPlusDividerPercent
+            } else {
+                translationY = banner.height.toFloat()
+            }
         }
-
         return Pair(translationX, translationY)
     }
 
@@ -266,6 +277,10 @@
         iconAppChipView.setRotation(degreesRotated)
     }
 
+    /**
+     * @param inSplitSelection Whether user currently has a task from this task group staged for
+     * split screen. Currently this state is not reachable in fake seascape.
+     */
     override fun measureGroupedTaskViewThumbnailBounds(
         primarySnapshot: View,
         secondarySnapshot: View,
@@ -273,7 +288,8 @@
         parentHeight: Int,
         splitBoundsConfig: SplitBounds,
         dp: DeviceProfile,
-        isRtl: Boolean
+        isRtl: Boolean,
+        inSplitSelection: Boolean
     ) {
         val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
         val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
@@ -339,6 +355,7 @@
         if (isRtl) displacement > 0 else displacement < 0
 
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
+
     /* -------------------- */
 
     override fun getSplitIconsPosition(
diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
new file mode 100644
index 0000000..df546ca
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
@@ -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.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+
+/** Notifies added callbacks that high res state has changed */
+interface HighResLoadingStateNotifier {
+    /** Adds a callback for high res loading state */
+    fun addCallback(callback: HighResLoadingStateChangedCallback)
+
+    /** Removes a callback for high res loading state */
+    fun removeCallback(callback: HighResLoadingStateChangedCallback)
+}
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/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
new file mode 100644
index 0000000..d2cb595
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
@@ -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.
+ */
+
+package com.android.quickstep.recents.data
+
+/**
+ * Container to hold [com.android.launcher3.DeviceProfile] related to Recents.
+ *
+ * @property isLargeScreen whether the current device posture has a large screen
+ */
+data class RecentsDeviceProfile(
+    val isLargeScreen: Boolean,
+)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt
new file mode 100644
index 0000000..13cf56d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+interface RecentsDeviceProfileRepository {
+    fun getRecentsDeviceProfile(): RecentsDeviceProfile
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
new file mode 100644
index 0000000..c64453d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.views.RecentsViewContainer
+
+/**
+ * Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains
+ * data related to Recents.
+ */
+class RecentsDeviceProfileRepositoryImpl(private val container: RecentsViewContainer) :
+    RecentsDeviceProfileRepository {
+
+    override fun getRecentsDeviceProfile() =
+        with(container.deviceProfile) { RecentsDeviceProfile(isLargeScreen = isTablet) }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt
new file mode 100644
index 0000000..2c2a744
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.view.Surface
+
+/**
+ * Container to hold orientation/rotation related information related to Recents.
+ *
+ * @property activityRotation rotation of the activity hosting RecentsView
+ */
+data class RecentsRotationState(
+    @Surface.Rotation val activityRotation: Int,
+    @Surface.Rotation val orientationHandlerRotation: Int,
+)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt
new file mode 100644
index 0000000..ed074d2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+interface RecentsRotationStateRepository {
+    fun getRecentsRotationState(): RecentsRotationState
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt
new file mode 100644
index 0000000..8417b06
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.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.quickstep.recents.data
+
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * Repository for [RecentsRotationState] which holds orientation/rotation related information
+ * related to Recents
+ */
+class RecentsRotationStateRepositoryImpl(private val state: RecentsOrientedState) :
+    RecentsRotationStateRepository {
+    override fun getRecentsRotationState() =
+        with(state) {
+            RecentsRotationState(
+                activityRotation = recentsActivityRotation,
+                orientationHandlerRotation = orientationHandler.rotation
+            )
+        }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt
new file mode 100644
index 0000000..6e7789d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt
@@ -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.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.TaskVisualsChangeListener
+
+/** Notifies added listeners that task visuals have changed */
+interface TaskVisualsChangeNotifier {
+    /** Adds a listener for visuals changes */
+    fun addThumbnailChangeListener(listener: TaskVisualsChangeListener)
+
+    /** Removes a listener for visuals changes */
+    fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener)
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
new file mode 100644
index 0000000..a45d194
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.os.UserHandle
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
+import com.android.quickstep.util.TaskVisualsChangeListener
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import java.util.concurrent.ConcurrentHashMap
+
+/** Delegates the checking of task visuals (thumbnails, high res changes, icons) */
+interface TaskVisualsChangedDelegate :
+    TaskVisualsChangeListener, HighResLoadingStateChangedCallback {
+    /** Registers a callback for visuals relating to icons */
+    fun registerTaskIconChangedCallback(
+        taskKey: Task.TaskKey,
+        taskIconChangedCallback: TaskIconChangedCallback,
+    )
+
+    /** Unregisters a callback for visuals relating to icons */
+    fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey)
+
+    /** Registers a callback for visuals relating to thumbnails */
+    fun registerTaskThumbnailChangedCallback(
+        taskKey: Task.TaskKey,
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback,
+    )
+
+    /** Unregisters a callback for visuals relating to thumbnails */
+    fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey)
+
+    /** A callback for task icon changes */
+    interface TaskIconChangedCallback {
+        /** Informs the listener that the task icon has changed */
+        fun onTaskIconChanged()
+    }
+
+    /** A callback for task thumbnail changes */
+    interface TaskThumbnailChangedCallback {
+        /** Informs the listener that the task thumbnail data has changed to [thumbnailData] */
+        fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?)
+
+        /** Informs the listener that the default resolution for loading thumbnails has changed */
+        fun onHighResLoadingStateChanged()
+    }
+}
+
+class TaskVisualsChangedDelegateImpl(
+    private val taskVisualsChangeNotifier: TaskVisualsChangeNotifier,
+    private val highResLoadingStateNotifier: HighResLoadingStateNotifier,
+) : TaskVisualsChangedDelegate {
+    private val taskIconChangedCallbacks =
+        ConcurrentHashMap<Int, Pair<Task.TaskKey, TaskIconChangedCallback>>()
+    private val taskThumbnailChangedCallbacks =
+        ConcurrentHashMap<Int, Pair<Task.TaskKey, TaskThumbnailChangedCallback>>()
+
+    override fun onTaskIconChanged(taskId: Int) {
+        taskIconChangedCallbacks[taskId]?.let { (_, callback) -> callback.onTaskIconChanged() }
+    }
+
+    override fun onTaskIconChanged(pkg: String, user: UserHandle) {
+        taskIconChangedCallbacks.values
+            .filter { (taskKey, _) ->
+                pkg == taskKey.packageName && user.identifier == taskKey.userId
+            }
+            .forEach { (_, callback) -> callback.onTaskIconChanged() }
+    }
+
+    override fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData?): Task? {
+        taskThumbnailChangedCallbacks[taskId]?.let { (_, callback) ->
+            callback.onTaskThumbnailChanged(thumbnailData)
+        }
+        return null
+    }
+
+    override fun onHighResLoadingStateChanged(enabled: Boolean) {
+        taskThumbnailChangedCallbacks.values.forEach { (_, callback) ->
+            callback.onHighResLoadingStateChanged()
+        }
+    }
+
+    override fun registerTaskIconChangedCallback(
+        taskKey: Task.TaskKey,
+        taskIconChangedCallback: TaskIconChangedCallback,
+    ) {
+        updateCallbacks {
+            taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback
+        }
+    }
+
+    override fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) {
+        updateCallbacks { taskIconChangedCallbacks.remove(taskKey.id) }
+    }
+
+    override fun registerTaskThumbnailChangedCallback(
+        taskKey: Task.TaskKey,
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback,
+    ) {
+        updateCallbacks {
+            taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback
+        }
+    }
+
+    override fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) {
+        updateCallbacks { taskThumbnailChangedCallbacks.remove(taskKey.id) }
+    }
+
+    @Synchronized
+    private fun updateCallbacks(callbackModifier: () -> Unit) {
+        val prevHasCallbacks =
+            taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size > 0
+        callbackModifier()
+
+        val currHasCallbacks =
+            taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size > 0
+
+        when {
+            prevHasCallbacks && !currHasCallbacks -> {
+                taskVisualsChangeNotifier.removeThumbnailChangeListener(this)
+                highResLoadingStateNotifier.removeCallback(this)
+            }
+            !prevHasCallbacks && currHasCallbacks -> {
+                taskVisualsChangeNotifier.addThumbnailChangeListener(this)
+                highResLoadingStateNotifier.addCallback(this)
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index f73db5a..6c627ef 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -17,160 +17,201 @@
 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.ExperimentalCoroutinesApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChangedBy
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+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,
+    private val recentsCoroutineScope: CoroutineScope,
+    private val dispatcherProvider: DispatcherProvider,
 ) : RecentTasksRepository {
-    private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
-    private val _taskData =
-        groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
-    private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
-
-    private val taskData: Flow<List<Task>> =
-        combine(_taskData, getThumbnailQueryResults(), getIconQueryResults()) {
-            tasks,
-            thumbnailQueryResults,
-            iconQueryResults ->
-            tasks.forEach { task ->
-                // Add retrieved thumbnails + remove unnecessary thumbnails
-                task.thumbnail = thumbnailQueryResults[task.key.id]
-
-                // TODO(b/352331675) don't load icons for DesktopTaskView
-                // Add retrieved icons + remove unnecessary icons
-                task.icon = iconQueryResults[task.key.id]?.icon
-                task.titleDescription = iconQueryResults[task.key.id]?.contentDescription
-                task.title = iconQueryResults[task.key.id]?.title
-            }
-            tasks
-        }
+    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 taskData
+        return tasks.map { it.values.toList() }
     }
 
-    override fun getTaskDataById(taskId: Int): Flow<Task?> =
-        taskData.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 =
-        flow {
-                emit(task.key.id to task.thumbnail)
-                val thumbnailDataResult: ThumbnailData? =
-                    suspendCancellableCoroutine { continuation ->
-                        val cancellableTask =
-                            taskThumbnailDataSource.getThumbnailInBackground(task) {
-                                continuation.resume(it)
-                            }
-                        continuation.invokeOnCancellation { cancellableTask?.cancel() }
-                    }
-                emit(task.key.id to thumbnailDataResult)
-            }
-            .distinctUntilChanged()
+    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)
+                },
+            )
+    }
 
-    /**
-     * This is a Flow that makes a query for thumbnail data to the [taskThumbnailDataSource] for
-     * each visible task. It then collects the responses and returns them in a Map as soon as they
-     * are available.
-     */
-    private fun getThumbnailQueryResults(): Flow<Map<Int, ThumbnailData?>> {
-        val visibleTasks =
-            combine(_taskData, visibleTaskIds) { tasks, visibleIds ->
-                tasks.filter { it.key.id in visibleIds }
-            }
-        val visibleThumbnailDataRequests: Flow<List<ThumbnailDataRequest>> =
-            visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
-        return visibleThumbnailDataRequests.flatMapLatest {
-            thumbnailRequestFlows: List<ThumbnailDataRequest> ->
-            if (thumbnailRequestFlows.isEmpty()) {
-                flowOf(emptyMap())
-            } else {
-                combine(thumbnailRequestFlows) { it.toMap() }
+    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) }
     }
 
-    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
-    private fun getIconDataRequest(task: Task): IconDataRequest =
-        flow {
-                emit(task.key.id to task.getTaskIconQueryResponse())
-                val iconDataResponse: TaskIconQueryResponse? =
-                    suspendCancellableCoroutine { continuation ->
-                        val cancellableTask =
-                            taskIconDataSource.getIconInBackground(task) {
-                                icon,
-                                contentDescription,
-                                title ->
-                                continuation.resume(
-                                    TaskIconQueryResponse(icon, contentDescription, title)
-                                )
-                            }
-                        continuation.invokeOnCancellation { cancellableTask?.cancel() }
+    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))
                     }
-                emit(task.key.id to iconDataResponse)
-            }
-            .distinctUntilChanged()
+                }
+            },
+        )
+    }
 
-    private fun getIconQueryResults(): Flow<Map<Int, TaskIconQueryResponse?>> {
-        val visibleTasks =
-            combine(_taskData, visibleTaskIds) { tasks, visibleIds ->
-                tasks.filter { it.key.id in visibleIds }
-            }
-        val visibleIconDataRequests: Flow<List<IconDataRequest>> =
-            visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
-        return visibleIconDataRequests.flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
-            if (iconRequestFlows.isEmpty()) {
-                flowOf(emptyMap())
-            } else {
-                combine(iconRequestFlows) { it.toMap() }
+    private suspend fun fetchThumbnail(task: Task) {
+        updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
+        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(
+            task.key,
+            object : TaskThumbnailChangedCallback {
+                override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) {
+                    updateThumbnail(task.key.id, thumbnailData)
+                }
+
+                override fun onHighResLoadingStateChanged() {
+                    recentsCoroutineScope.launch {
+                        updateThumbnail(task.key.id, getThumbnailFromDataSource(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)) }
+    }
+
+    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) {
+            suspendCancellableCoroutine { continuation ->
+                val cancellableTask =
+                    taskThumbnailDataSource.getThumbnailInBackground(task) {
+                        continuation.resume(it)
+                    }
+                continuation.invokeOnCancellation { cancellableTask?.cancel() }
             }
         }
+
+    private suspend fun getIconFromDataSource(task: Task) =
+        withContext(dispatcherProvider.main) {
+            suspendCancellableCoroutine { continuation ->
+                val cancellableTask =
+                    taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title
+                        ->
+                        icon.constantState?.let {
+                            continuation.resume(
+                                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,
+    )
 }
-
-private data class TaskIconQueryResponse(
-    val icon: Drawable,
-    val contentDescription: String,
-    val title: String
-)
-
-private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
-    val iconVal = icon ?: return null
-    val titleDescriptionVal = titleDescription ?: return null
-    val titleVal = title ?: return null
-
-    return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal)
-}
-
-private typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
-
-private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
new file mode 100644
index 0000000..dd11d48
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.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.quickstep.recents.di
+
+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
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl
+import com.android.quickstep.recents.data.TasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
+import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.task.viewmodel.TaskViewModel
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+internal typealias RecentsScopeId = String
+
+class RecentsDependencies private constructor(private val appContext: Context) {
+    private val scopes = mutableMapOf<RecentsScopeId, RecentsDependenciesScope>()
+
+    init {
+        startDefaultScope(appContext)
+    }
+
+    /**
+     * This function initialised the default scope with RecentsView dependencies. These dependencies
+     * are used multiple times and should be a singleton to share across Recents classes.
+     */
+    private fun startDefaultScope(appContext: Context) {
+        createScope(DEFAULT_SCOPE_ID).apply {
+            set(RecentsViewData::class.java.simpleName, RecentsViewData())
+            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,
+                )
+            set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate)
+
+            // Create RecentsTaskRepository singleton
+            val recentTasksRepository: RecentTasksRepository =
+                with(recentsModel) {
+                    TasksRepository(
+                        this,
+                        thumbnailCache,
+                        iconCache,
+                        taskVisualsChangedDelegate,
+                        recentsCoroutineScope,
+                        ProductionDispatchers,
+                    )
+                }
+            set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
+        }
+    }
+
+    inline fun <reified T> inject(
+        scopeId: RecentsScopeId = "",
+        extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+        noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+    ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory)
+
+    @Suppress("UNCHECKED_CAST")
+    @JvmOverloads
+    fun <T> inject(
+        modelClass: Class<T>,
+        scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+        extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+        factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+    ): T {
+        val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID }
+        val scope = scopes[currentScopeId] ?: createScope(currentScopeId)
+
+        log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO)
+        var instance: T?
+        synchronized(this) {
+            instance = getDependency(scope, modelClass)
+            log("found instance? $instance", Log.INFO)
+            if (instance == null) {
+                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!!
+    }
+
+    inline fun <reified T> provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T =
+        provide(T::class.java, scopeId = scopeId, factory = factory)
+
+    @JvmOverloads
+    fun <T> provide(
+        modelClass: Class<T>,
+        scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+        factory: () -> T,
+    ) = inject(modelClass, scopeId, factory = { factory.invoke() })
+
+    private fun <T> getDependency(scope: RecentsDependenciesScope, modelClass: Class<T>): T? {
+        var instance: T? = scope[modelClass.simpleName] as T?
+        if (instance == null) {
+            instance =
+                scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId ->
+                    getScope(scopeId)[modelClass.simpleName]
+                } as T?
+        }
+        if (instance != null) log("Found dependency: $instance", Log.INFO)
+        return instance
+    }
+
+    fun getScope(scope: Any): RecentsDependenciesScope {
+        val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+        return getScope(scopeId)
+    }
+
+    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")
+    private fun <T> createDependency(
+        modelClass: Class<T>,
+        scopeId: RecentsScopeId,
+        extras: RecentsDependenciesExtras,
+    ): T {
+        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 -> {
+                    with(RecentsModel.INSTANCE.get(appContext)) {
+                        TasksRepository(
+                            this,
+                            thumbnailCache,
+                            iconCache,
+                            get(),
+                            get(),
+                            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 ->
+                    TaskThumbnailViewModelImpl(
+                        recentsViewData = inject(),
+                        taskViewData = inject(scopeId, extras),
+                        taskContainerData = inject(scopeId),
+                        dispatcherProvider = inject(),
+                        getThumbnailPositionUseCase = inject(),
+                        tasksRepository = inject(),
+                        splashAlphaUseCase = inject(scopeId),
+                    )
+                TaskOverlayViewModel::class.java -> {
+                    val task = extras["Task"] as Task
+                    TaskOverlayViewModel(
+                        task = task,
+                        recentsViewData = inject(),
+                        recentTasksRepository = inject(),
+                        getThumbnailPositionUseCase = inject(),
+                    )
+                }
+                GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
+                SysUiStatusNavFlagsUseCase::class.java ->
+                    SysUiStatusNavFlagsUseCase(taskRepository = inject())
+                GetThumbnailPositionUseCase::class.java ->
+                    GetThumbnailPositionUseCase(
+                        deviceProfileRepository = inject(),
+                        rotationStateRepository = inject(),
+                        tasksRepository = inject(),
+                    )
+                SplashAlphaUseCase::class.java ->
+                    SplashAlphaUseCase(
+                        recentsViewData = inject(),
+                        taskContainerData = inject(scopeId),
+                        taskThumbnailViewData = inject(scopeId),
+                        tasksRepository = inject(),
+                        rotationStateRepository = inject(),
+                    )
+                else -> {
+                    log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
+                    error("Factory for ${modelClass.simpleName} not defined!")
+                }
+            }
+        return (instance as T).also {
+            log(
+                "createDependency ${modelClass.simpleName} with $scopeId and $extras completed",
+                Log.WARN,
+            )
+        }
+    }
+
+    private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
+        return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it }
+    }
+
+    private fun log(message: String, @Log.Level level: Int = Log.DEBUG) {
+        if (DEBUG) {
+            when (level) {
+                Log.WARN -> Log.w(TAG, message)
+                Log.VERBOSE -> Log.v(TAG, message)
+                Log.INFO -> Log.i(TAG, message)
+                Log.ERROR -> Log.e(TAG, message)
+                else -> Log.d(TAG, message)
+            }
+        }
+    }
+
+    companion object {
+        private const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope"
+        private const val TAG = "RecentsDependencies"
+        private const val DEBUG = false
+
+        @Volatile private lateinit var instance: RecentsDependencies
+
+        fun initialize(view: View): RecentsDependencies = initialize(view.context)
+
+        fun initialize(context: Context): RecentsDependencies {
+            synchronized(this) { instance = RecentsDependencies(context.applicationContext) }
+            return instance
+        }
+
+        fun getInstance(): RecentsDependencies {
+            if (!Companion::instance.isInitialized) {
+                throw UninitializedPropertyAccessException(
+                    "Recents dependencies are not initialized. " +
+                        "Call `RecentsDependencies.initialize` before using this container."
+                )
+            }
+            return instance
+        }
+
+        fun destroy() {
+            instance.scopes.clear()
+            instance.startDefaultScope(instance.appContext)
+        }
+    }
+}
+
+inline fun <reified T> RecentsDependencies.Companion.inject(
+    scope: Any = "",
+    vararg extras: Pair<String, Any>,
+    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): Lazy<T> = lazy { get(scope, RecentsDependenciesExtras(extras), factory) }
+
+inline fun <reified T> RecentsDependencies.Companion.get(
+    scope: Any = "",
+    extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T {
+    val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+    return getInstance().inject(scopeId, extras, factory)
+}
+
+inline fun <reified T> RecentsDependencies.Companion.get(
+    scope: Any = "",
+    vararg extras: Pair<String, Any>,
+    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T = get(scope, RecentsDependenciesExtras(extras), factory)
+
+fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId)
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
new file mode 100644
index 0000000..753cb6e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+data class RecentsDependenciesExtras(private val data: MutableMap<String, Any> = mutableMapOf()) {
+    constructor(value: Array<out Pair<String, Any>>) : this(value.toMap().toMutableMap())
+
+    operator fun get(key: String) = data[key]
+
+    operator fun set(key: String, value: Any) {
+        data[key] = value
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
new file mode 100644
index 0000000..56bb1ed
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+import android.util.Log
+
+class RecentsDependenciesScope(
+    val scopeId: RecentsScopeId,
+    private val dependencies: MutableMap<String, Any> = mutableMapOf(),
+    private val scopeIds: MutableList<RecentsScopeId> = mutableListOf()
+) {
+    val scopeIdsLinked: List<RecentsScopeId>
+        get() = scopeIds.toList()
+
+    operator fun get(identifier: String): Any? {
+        log("get $identifier")
+        return dependencies[identifier]
+    }
+
+    operator fun set(key: String, value: Any) {
+        synchronized(this) {
+            log("set $key")
+            dependencies[key] = value
+        }
+    }
+
+    fun remove(key: String): Any? {
+        synchronized(this) {
+            log("remove $key")
+            return dependencies.remove(key)
+        }
+    }
+
+    fun linkTo(scope: RecentsDependenciesScope) {
+        log("linking to ${scope.scopeId}")
+        scopeIds += scope.scopeId
+    }
+
+    fun close() {
+        log("reset")
+        synchronized(this) { dependencies.clear() }
+    }
+
+    private fun log(message: String) {
+        if (DEBUG) Log.d(TAG, "[scopeId=$scopeId] $message")
+    }
+
+    override fun toString(): String =
+        "scopeId: $scopeId" +
+            "\n dependencies: ${dependencies.map { "${it.key}=${it.value}" }.joinToString(", ")}" +
+            "\n linked to: ${scopeIds.joinToString(", ")}"
+
+    private companion object {
+        private const val TAG = "RecentsDependenciesScope"
+        private const val DEBUG = false
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
new file mode 100644
index 0000000..f060d7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.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.quickstep.recents.usecase
+
+import android.graphics.Matrix
+import android.graphics.Rect
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import kotlinx.coroutines.flow.firstOrNull
+
+/** Use case for retrieving [Matrix] for positioning Thumbnail in a View */
+class GetThumbnailPositionUseCase(
+    private val deviceProfileRepository: RecentsDeviceProfileRepository,
+    private val rotationStateRepository: RecentsRotationStateRepository,
+    private val tasksRepository: RecentTasksRepository,
+    private val previewPositionHelper: PreviewPositionHelper = PreviewPositionHelper()
+) {
+    suspend fun run(taskId: Int, width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
+        val thumbnailData =
+            tasksRepository.getThumbnailById(taskId).firstOrNull() ?: return MissingThumbnail
+        val thumbnail = thumbnailData.thumbnail ?: return MissingThumbnail
+        previewPositionHelper.updateThumbnailMatrix(
+            Rect(0, 0, thumbnail.width, thumbnail.height),
+            thumbnailData,
+            width,
+            height,
+            deviceProfileRepository.getRecentsDeviceProfile().isLargeScreen,
+            rotationStateRepository.getRecentsRotationState().activityRotation,
+            isRtl
+        )
+        return MatrixScaling(
+            previewPositionHelper.matrix,
+            previewPositionHelper.isOrientationChanged
+        )
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
similarity index 95%
rename from quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
rename to quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
index e8dd04c3..3aa808e 100644
--- a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.quickstep.task.util
+package com.android.quickstep.recents.usecase
 
 import android.graphics.Bitmap
 import com.android.quickstep.recents.data.RecentTasksRepository
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
new file mode 100644
index 0000000..1d19c7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.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.recents.usecase
+
+import android.view.WindowInsetsController
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.quickstep.recents.data.RecentTasksRepository
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+/** UseCase to calculate flags for status bar and navigation bar */
+class SysUiStatusNavFlagsUseCase(private val taskRepository: RecentTasksRepository) {
+    fun getSysUiStatusNavFlags(taskId: Int): Int {
+        val thumbnailData =
+            runBlocking { taskRepository.getThumbnailById(taskId).firstOrNull() } ?: return 0
+
+        val thumbnailAppearance = thumbnailData.appearance
+        var flags = 0
+        flags =
+            flags or
+                if (
+                    thumbnailAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0
+                )
+                    FLAG_LIGHT_STATUS
+                else FLAG_DARK_STATUS
+        flags =
+            flags or
+                if (
+                    thumbnailAppearance and
+                        WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS != 0
+                )
+                    FLAG_LIGHT_NAV
+                else FLAG_DARK_NAV
+        return flags
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
new file mode 100644
index 0000000..1a1bef7
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
@@ -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.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.graphics.Matrix
+
+/** State on how a task Thumbnail can fit on given canvas */
+sealed class ThumbnailPositionState {
+    data object MissingThumbnail : ThumbnailPositionState()
+
+    data class MatrixScaling(val matrix: Matrix, val isRotated: Boolean) : ThumbnailPositionState()
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index fdb62df..87446b0 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -31,4 +31,15 @@
 
     // The settled set of visible taskIds that is updated after RecentsView scroll settles.
     val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
+
+    // Color tint on foreground scrim
+    val tintAmount = MutableStateFlow(0f)
+
+    val thumbnailSplashProgress = MutableStateFlow(0f)
+
+    // A list of taskIds that are associated with a RecentsAnimationController. */
+    val runningTaskIds = MutableStateFlow(emptySet<Int>())
+
+    // Whether we should use static screenshot instead of live tile for taskIds in [runningTaskIds]
+    val runningTaskShowScreenshot = MutableStateFlow(false)
 }
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
new file mode 100644
index 0000000..c511005
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.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.quickstep.recents.viewmodel
+
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.systemui.shared.recents.model.ThumbnailData
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+
+class RecentsViewModel(
+    private val recentsTasksRepository: RecentTasksRepository,
+    private val recentsViewData: RecentsViewData,
+) {
+    fun refreshAllTaskData() {
+        recentsTasksRepository.getAllTaskData(true)
+    }
+
+    fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
+        recentsTasksRepository.setVisibleTasks(visibleTaskIdList.toSet())
+    }
+
+    fun updateScale(scale: Float) {
+        recentsViewData.scale.value = scale
+    }
+
+    fun updateFullscreenProgress(fullscreenProgress: Float) {
+        recentsViewData.fullscreenProgress.value = fullscreenProgress
+    }
+
+    fun updateTasksFullyVisible(taskIds: Set<Int>) {
+        recentsViewData.settledFullyVisibleTaskIds.value = taskIds
+    }
+
+    fun setOverlayEnabled(isOverlayEnabled: Boolean) {
+        recentsViewData.overlayEnabled.value = isOverlayEnabled
+    }
+
+    fun setTintAmount(tintAmount: Float) {
+        recentsViewData.tintAmount.value = tintAmount
+    }
+
+    fun updateThumbnailSplashProgress(taskThumbnailSplashAlpha: Float) {
+        recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
+    }
+
+    suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>?) {
+        if (updatedThumbnails.isNullOrEmpty()) return
+        combine(
+                updatedThumbnails.map {
+                    recentsTasksRepository.getThumbnailById(it.key).filter { thumbnailData ->
+                        thumbnailData?.snapshotId == it.value.snapshotId
+                    }
+                }
+            ) {}
+            .first()
+    }
+
+    suspend fun waitForRunningTaskShowScreenshotToUpdate() {
+        recentsViewData.runningTaskShowScreenshot.filter { it }.first()
+    }
+
+    fun onReset() {
+        updateVisibleTasks(emptyList())
+    }
+
+    fun updateRunningTask(taskIds: Set<Int>) {
+        recentsViewData.runningTaskIds.value = taskIds
+    }
+
+    fun setRunningTaskShowScreenshot(showScreenshot: Boolean) {
+        recentsViewData.runningTaskShowScreenshot.value = showScreenshot
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
new file mode 100644
index 0000000..168c1e0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
@@ -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.quickstep.recents.viewmodel
+
+import android.graphics.Bitmap
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+class TaskContainerViewModel(
+    private val sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase,
+    private val getThumbnailUseCase: GetThumbnailUseCase,
+    private val splashAlphaUseCase: SplashAlphaUseCase,
+) {
+    fun getThumbnail(taskId: Int): Bitmap? = getThumbnailUseCase.run(taskId)
+
+    fun getSysUiStatusNavFlags(taskId: Int) =
+        sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(taskId)
+
+    fun shouldShowThumbnailSplash(taskId: Int): Boolean =
+        (runBlocking { splashAlphaUseCase.execute(taskId).firstOrNull() } ?: 0f) > 0f
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
new file mode 100644
index 0000000..7673c71
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.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.graphics.Bitmap
+import android.view.Surface
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.Utilities
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+class SplashAlphaUseCase(
+    private val recentsViewData: RecentsViewData,
+    private val taskContainerData: TaskContainerData,
+    private val taskThumbnailViewData: TaskThumbnailViewData,
+    private val tasksRepository: RecentTasksRepository,
+    private val rotationStateRepository: RecentsRotationStateRepository,
+) {
+    fun execute(taskId: Int): Flow<Float> =
+        combine(
+                taskThumbnailViewData.width,
+                taskThumbnailViewData.height,
+                tasksRepository.getThumbnailById(taskId),
+                taskContainerData.thumbnailSplashProgress,
+                recentsViewData.thumbnailSplashProgress
+            ) { width, height, thumbnailData, taskSplashProgress, globalSplashProgress ->
+                val thumbnail = thumbnailData?.thumbnail
+                when {
+                    thumbnail == null -> 0f
+                    taskSplashProgress > 0f -> taskSplashProgress
+                    globalSplashProgress > 0f &&
+                        isInaccurateThumbnail(thumbnail, thumbnailData.rotation, width, height) ->
+                        globalSplashProgress
+                    else -> 0f
+                }
+            }
+            .distinctUntilChanged()
+
+    private fun isInaccurateThumbnail(
+        thumbnail: Bitmap,
+        thumbnailRotation: Int,
+        width: Int,
+        height: Int
+    ): Boolean {
+        return isThumbnailAspectRatioDifferentFromThumbnailData(thumbnail, width, height) ||
+            isThumbnailRotationDifferentFromTask(thumbnailRotation)
+    }
+
+    private fun isThumbnailAspectRatioDifferentFromThumbnailData(
+        thumbnail: Bitmap,
+        viewWidth: Int,
+        viewHeight: Int
+    ): Boolean {
+        val viewAspect: Float = viewWidth / viewHeight.toFloat()
+        val thumbnailAspect: Float = thumbnail.width / thumbnail.height.toFloat()
+        return Utilities.isRelativePercentDifferenceGreaterThan(
+            viewAspect,
+            thumbnailAspect,
+            PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT
+        )
+    }
+
+    private fun isThumbnailRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
+        val rotationState = rotationStateRepository.getRecentsRotationState()
+        return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
+            (rotationState.activityRotation - thumbnailRotation) % 2 != 0
+        } else {
+            rotationState.orientationHandlerRotation != thumbnailRotation
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
index feee11f..5fb5b90 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
@@ -17,15 +17,10 @@
 package com.android.quickstep.task.thumbnail
 
 import android.graphics.Bitmap
-import android.graphics.Matrix
 
 /** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
 sealed class TaskOverlayUiState {
     data object Disabled : TaskOverlayUiState()
 
-    data class Enabled(
-        val isRealSnapshot: Boolean,
-        val thumbnail: Bitmap?,
-        val thumbnailMatrix: Matrix
-    ) : TaskOverlayUiState()
+    data class Enabled(val isRealSnapshot: Boolean, val thumbnail: Bitmap?) : TaskOverlayUiState()
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 40f9b28..36a86f2 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -17,18 +17,25 @@
 package com.android.quickstep.task.thumbnail
 
 import android.graphics.Bitmap
-import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.view.Surface
 import androidx.annotation.ColorInt
 
 sealed class TaskThumbnailUiState {
     data object Uninitialized : TaskThumbnailUiState()
+
     data object LiveTile : TaskThumbnailUiState()
+
     data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
+
+    data class SnapshotSplash(
+        val snapshot: Snapshot,
+        val splash: Drawable?,
+    ) : TaskThumbnailUiState()
+
     data class Snapshot(
         val bitmap: Bitmap,
-        val drawnRect: Rect,
+        @Surface.Rotation val thumbnailRotation: Int,
         @ColorInt val backgroundColor: Int
-    ) : TaskThumbnailUiState()
+    )
 }
-
-data class TaskThumbnail(val taskId: Int, val isRunning: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index d22fc94..a8c8659 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -22,23 +22,25 @@
 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 android.widget.FrameLayout
-import android.widget.ImageView
 import androidx.annotation.ColorInt
-import androidx.core.view.isVisible
+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.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.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
-import com.android.quickstep.views.TaskView
+import com.android.quickstep.views.FixedSizeImageView
 import com.android.systemui.shared.system.QuickStepContract
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
@@ -48,27 +50,19 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 
-class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
-    // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
-    //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
-    //  This is using a lazy for now because the dependencies cannot be obtained without DI.
-    val viewModel by lazy {
-        val recentsView =
-            RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
-                .getOverviewPanel<RecentsView<*, *>>()
-        TaskThumbnailViewModel(
-            recentsView.mRecentsViewData!!,
-            (parent as TaskView).taskViewData,
-            (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
-            recentsView.mTasksRepository!!,
-        )
-    }
+class TaskThumbnailView : ConstraintLayout, ViewPool.Reusable {
+    private lateinit var viewData: TaskThumbnailViewData
+    private lateinit var viewModel: TaskThumbnailViewModel
+
     private lateinit var viewAttachedScope: CoroutineScope
 
     private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
     private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
-    private val thumbnail: ImageView by lazy { findViewById(R.id.task_thumbnail) }
+    private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) }
+    private val splashBackground: View by lazy { findViewById(R.id.splash_background) }
+    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()
@@ -95,21 +89,29 @@
         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) {
                     is Uninitialized -> {}
                     is LiveTile -> drawLiveWindow()
-                    is Snapshot -> drawSnapshot(viewModelUiState)
+                    is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
                     is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
                 }
             }
             .launchIn(viewAttachedScope)
         viewModel.dimProgress
-            .onEach { dimProgress ->
-                // TODO(b/348195366) Add fade in/out for scrim
-                scrimView.alpha = dimProgress * MAX_SCRIM_ALPHA
+            .onEach { dimProgress -> scrimView.alpha = dimProgress }
+            .launchIn(viewAttachedScope)
+        viewModel.splashAlpha
+            .onEach { splashAlpha ->
+                splashBackground.alpha = splashAlpha
+                splashIcon.alpha = splashAlpha
             }
             .launchIn(viewAttachedScope)
         viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope)
@@ -135,7 +137,38 @@
     }
 
     override fun onRecycle() {
-        // Do nothing
+        uiState = Uninitialized
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        if (changed) {
+            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()
+        }
+    }
+
+    override fun setScaleX(scaleX: Float) {
+        super.setScaleX(scaleX)
+        // Splash icon should ignore scale on TTV
+        splashIcon.scaleX = 1 / scaleX
+    }
+
+    override fun setScaleY(scaleY: Float) {
+        super.setScaleY(scaleY)
+        // Splash icon should ignore scale on TTV
+        splashIcon.scaleY = 1 / scaleY
     }
 
     override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -147,8 +180,10 @@
     }
 
     private fun resetViews() {
-        liveTileView.isVisible = false
-        thumbnail.isVisible = false
+        liveTileView.isInvisible = true
+        thumbnailView.isInvisible = true
+        splashBackground.alpha = 0f
+        splashIcon.alpha = 0f
         scrimView.alpha = 0f
         setBackgroundColor(Color.BLACK)
     }
@@ -158,23 +193,35 @@
     }
 
     private fun drawLiveWindow() {
-        liveTileView.isVisible = true
+        liveTileView.isInvisible = false
+    }
+
+    private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) {
+        drawSnapshot(snapshotSplash.snapshot)
+
+        splashBackground.setBackgroundColor(snapshotSplash.snapshot.backgroundColor)
+        splashIcon.setImageDrawable(snapshotSplash.splash)
     }
 
     private fun drawSnapshot(snapshot: Snapshot) {
         drawBackground(snapshot.backgroundColor)
-        thumbnail.setImageBitmap(snapshot.bitmap)
-        thumbnail.isVisible = true
+        thumbnailView.setImageBitmap(snapshot.bitmap)
+        thumbnailView.isInvisible = false
+        setImageMatrix()
+    }
+
+    private fun setImageMatrix() {
+        thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
     }
 
     private fun getCurrentCornerRadius() =
         Utilities.mapRange(
             viewModel.cornerRadiusProgress.value,
             overviewCornerRadius,
-            fullscreenCornerRadius
+            fullscreenCornerRadius,
         ) / inheritedScale
 
     private companion object {
-        const val MAX_SCRIM_ALPHA = 0.4f
+        const val TAG = "TaskThumbnailView"
     }
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
new file mode 100644
index 0000000..3502029
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class TaskThumbnailViewData {
+    val width = MutableStateFlow(0)
+    val height = MutableStateFlow(0)
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
deleted file mode 100644
index d8729a6..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,101 +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.annotation.ColorInt
-import android.graphics.Rect
-import androidx.core.graphics.ColorUtils
-import com.android.quickstep.recents.data.RecentTasksRepository
-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.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.quickstep.task.viewmodel.TaskViewData
-import com.android.systemui.shared.recents.model.Task
-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
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class TaskThumbnailViewModel(
-    recentsViewData: RecentsViewData,
-    taskViewData: TaskViewData,
-    taskContainerData: TaskContainerData,
-    private val tasksRepository: RecentTasksRepository,
-) {
-    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
-    private var boundTaskIsRunning = false
-
-    /**
-     * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
-     * corner radius.
-     */
-    val cornerRadiusProgress =
-        if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
-        else MutableStateFlow(1f).asStateFlow()
-    val inheritedScale =
-        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
-            recentsScale * taskScale
-        }
-    val dimProgress: Flow<Float> = taskContainerData.taskMenuOpenProgress
-    val uiState: Flow<TaskThumbnailUiState> =
-        task
-            .flatMapLatest { taskFlow ->
-                taskFlow.map { taskVal ->
-                    when {
-                        taskVal == null -> Uninitialized
-                        boundTaskIsRunning -> LiveTile
-                        isBackgroundOnly(taskVal) ->
-                            BackgroundOnly(taskVal.colorBackground.removeAlpha())
-                        isSnapshotState(taskVal) -> {
-                            val bitmap = taskVal.thumbnail?.thumbnail!!
-                            Snapshot(
-                                bitmap,
-                                Rect(0, 0, bitmap.width, bitmap.height),
-                                taskVal.colorBackground.removeAlpha()
-                            )
-                        }
-                        else -> Uninitialized
-                    }
-                }
-            }
-            .distinctUntilChanged()
-
-    fun bind(taskThumbnail: TaskThumbnail) {
-        boundTaskIsRunning = taskThumbnail.isRunning
-        task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
-    }
-
-    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
-
-    private fun isSnapshotState(task: Task): Boolean {
-        val thumbnailPresent = task.thumbnail?.thumbnail != null
-        val taskLocked = task.isLocked
-
-        return thumbnailPresent && !taskLocked
-    }
-
-    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
-}
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index 5e55e2e..203177a 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -17,74 +17,98 @@
 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
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
-import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
 import com.android.systemui.shared.recents.model.Task
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
 
 /**
  * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper
  * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM.
  */
 class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) {
-    private lateinit var job: Job
+    private lateinit var overlayInitializedScope: CoroutineScope
     private var uiState: TaskOverlayUiState = Disabled
 
-    // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
-    //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
-    //  This is using a lazy for now because the dependencies cannot be obtained without DI.
-    private val taskOverlayViewModel by lazy {
-        val recentsView =
-            RecentsViewContainer.containerFromContext<RecentsViewContainer>(
-                    overlay.taskView.context
-                )
-                .getOverviewPanel<RecentsView<*, *>>()
-        TaskOverlayViewModel(task, recentsView.mRecentsViewData!!, recentsView.mTasksRepository!!)
-    }
+    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,
+        )
+
     fun init() {
-        // TODO(b/335396935): This should be changed to TaskView's scope.
-        job =
-            MainScope().launch {
-                taskOverlayViewModel.overlayState.collect {
-                    uiState = it
-                    if (it is Enabled) {
-                        Log.d(
-                            TAG,
-                            "initOverlay - taskId: ${task.key.id}, thumbnail: ${it.thumbnail}"
-                        )
-                        overlay.initOverlay(
-                            task,
-                            it.thumbnail,
-                            it.thumbnailMatrix,
-                            /* rotated= */ false
-                        )
-                    } else {
-                        Log.d(TAG, "reset - taskId: ${task.key.id}")
-                        overlay.reset()
-                    }
+        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
+                if (it is Enabled) {
+                    initOverlay(it)
+                } else {
+                    reset()
                 }
             }
+            .launchIn(overlayInitializedScope)
+        overlay.snapshotView.addOnLayoutChangeListener(snapshotLayoutChangeListener)
+    }
+
+    private fun initOverlay(enabledState: Enabled) {
+        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() {
+        if (DEBUG) {
+            Log.d(TAG, "reset - taskId: ${task.key.id}")
+        }
+        overlay.reset()
     }
 
     fun destroy() {
-        job.cancel()
+        overlayInitializedScope.cancel()
         uiState = Disabled
-        overlay.reset()
+        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/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
index 769424c..5f2de94 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
@@ -20,4 +20,6 @@
 
 class TaskContainerData {
     val taskMenuOpenProgress = MutableStateFlow(0f)
+
+    val thumbnailSplashProgress = MutableStateFlow(0f)
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 47f32fb..4e13d1c 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -18,6 +18,9 @@
 
 import android.graphics.Matrix
 import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
@@ -25,31 +28,52 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
 
 /** View model for TaskOverlay */
 class TaskOverlayViewModel(
-    task: Task,
+    private val task: Task,
     recentsViewData: RecentsViewData,
-    tasksRepository: RecentTasksRepository,
+    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+    recentTasksRepository: RecentTasksRepository,
 ) {
     val overlayState =
         combine(
                 recentsViewData.overlayEnabled,
                 recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
-                tasksRepository.getThumbnailById(task.key.id)
+                recentTasksRepository.getThumbnailById(task.key.id)
             ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
                 if (isOverlayEnabled && isFullyVisible) {
                     Enabled(
                         isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
                         thumbnailData?.thumbnail,
-                        // TODO(b/343101424): Use PreviewPositionHelper, listen from a common source
-                        // with
-                        //  TaskThumbnailView.
-                        Matrix.IDENTITY_MATRIX
                     )
                 } else {
                     Disabled
                 }
             }
             .distinctUntilChanged()
+
+    fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
+        return runBlocking {
+            val matrix: Matrix
+            val isRotated: Boolean
+            when (
+                val thumbnailPositionState =
+                    getThumbnailPositionUseCase.run(task.key.id, width, height, isRtl)
+            ) {
+                is MatrixScaling -> {
+                    matrix = thumbnailPositionState.matrix
+                    isRotated = thumbnailPositionState.isRotated
+                }
+                is MissingThumbnail -> {
+                    matrix = Matrix.IDENTITY_MATRIX
+                    isRotated = false
+                }
+            }
+            ThumbnailPositionState(matrix, isRotated)
+        }
+    }
+
+    data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean)
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
new file mode 100644
index 0000000..f55462a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import android.graphics.Matrix
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+/** ViewModel for representing TaskThumbnails */
+interface TaskThumbnailViewModel {
+    /**
+     * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
+     * corner radius.
+     */
+    val cornerRadiusProgress: StateFlow<Float>
+
+    /** The accumulated View.scale value for parent Views up to and including RecentsView */
+    val inheritedScale: Flow<Float>
+
+    /** Provides the level of dimming that the View should have */
+    val dimProgress: Flow<Float>
+
+    /** Provides the alpha of the splash icon */
+    val splashAlpha: Flow<Float>
+
+    /** Provides the UiState by which the task thumbnail can be represented */
+    val uiState: Flow<TaskThumbnailUiState>
+
+    /** Attaches this ViewModel to a specific task id for it to provide data from. */
+    fun bind(taskId: Int)
+
+    /** Returns a Matrix which can be applied to the snapshot */
+    fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
new file mode 100644
index 0000000..8b15a82
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language goveryning permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import android.annotation.ColorInt
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.graphics.Matrix
+import android.util.Log
+import androidx.core.graphics.ColorUtils
+import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.systemui.shared.recents.model.Task
+import kotlin.math.max
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class TaskThumbnailViewModelImpl(
+    recentsViewData: RecentsViewData,
+    taskViewData: TaskViewData,
+    taskContainerData: TaskContainerData,
+    dispatcherProvider: DispatcherProvider,
+    private val tasksRepository: RecentTasksRepository,
+    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+    private val splashAlphaUseCase: SplashAlphaUseCase,
+) : TaskThumbnailViewModel {
+    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
+    private val splashProgress = MutableStateFlow(flowOf(0f))
+    private var taskId: Int = INVALID_TASK_ID
+
+    override val cornerRadiusProgress =
+        if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
+        else MutableStateFlow(1f).asStateFlow()
+
+    override val inheritedScale =
+        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
+            recentsScale * taskScale
+        }
+
+    override val dimProgress: Flow<Float> =
+        combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
+            taskMenuOpenProgress,
+            tintAmount ->
+            max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
+        }
+
+    override val splashAlpha = splashProgress.flatMapLatest { it }
+
+    private val isLiveTile =
+        combine(
+                task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
+                recentsViewData.runningTaskIds,
+                recentsViewData.runningTaskShowScreenshot,
+            ) { taskId, runningTaskIds, runningTaskShowScreenshot ->
+                runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
+            }
+            .distinctUntilChanged()
+            .flowOn(dispatcherProvider.default)
+
+    override val uiState: Flow<TaskThumbnailUiState> =
+        combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning ->
+                // TODO(b/369339561) This log is firing a lot. Reduce emissions from TasksRepository
+                //  then re-enable this log.
+                //                Log.d(
+                //                    TAG,
+                //                    "Received task and / or live tile update. taskVal: $taskVal"
+                //                    + " isRunning: $isRunning.",
+                //                )
+                when {
+                    taskVal == null -> Uninitialized
+                    isRunning -> LiveTile
+                    isBackgroundOnly(taskVal) ->
+                        BackgroundOnly(taskVal.colorBackground.removeAlpha())
+                    isSnapshotSplashState(taskVal) ->
+                        SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
+                    else -> Uninitialized
+                }
+            }
+            .distinctUntilChanged()
+            .flowOn(dispatcherProvider.default)
+
+    override fun bind(taskId: Int) {
+        Log.d(TAG, "bind taskId: $taskId")
+        this.taskId = taskId
+        task.value = tasksRepository.getTaskDataById(taskId)
+        splashProgress.value = splashAlphaUseCase.execute(taskId)
+    }
+
+    override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
+        return runBlocking {
+            when (
+                val thumbnailPositionState =
+                    getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
+            ) {
+                is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
+                is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
+            }
+        }
+    }
+
+    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
+
+    private fun isSnapshotSplashState(task: Task): Boolean {
+        val thumbnailPresent = task.thumbnail?.thumbnail != null
+        val taskLocked = task.isLocked
+
+        return thumbnailPresent && !taskLocked
+    }
+
+    private fun createSnapshotState(task: Task): Snapshot {
+        val thumbnailData = task.thumbnail
+        val bitmap = thumbnailData?.thumbnail!!
+        return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
+    }
+
+    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
+
+    private companion object {
+        const val MAX_SCRIM_ALPHA = 0.4f
+        const val TAG = "TaskThumbnailViewModel"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
new file mode 100644
index 0000000..ec75d59
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class TaskViewModel(private val taskViewData: TaskViewData) : ViewModel() {
+    fun updateScale(scale: Float) {
+        taskViewData.scale.value = scale
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 8e3d44f..31aca03 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -17,18 +17,28 @@
 package com.android.quickstep.util;
 
 import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.animation.AnimatorSet;
+import android.annotation.NonNull;
 import android.os.Bundle;
 import android.os.IRemoteCallback;
 import android.view.animation.Interpolator;
 
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.RunnableList;
+import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * Utility class containing methods to help manage animations, interpolators, and timings.
  */
 public class AnimUtils {
+    private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
+
     /**
      * Fetches device-specific timings for the Overview > Split animation
      * (splitscreen initiated from Overview).
@@ -59,6 +69,33 @@
     }
 
     /**
+     * Synchronizes the timing for the split dismiss animation to the current transition to
+     * NORMAL (launcher home/workspace)
+     */
+    public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager,
+            @NonNull RecentsViewContainer container,
+            @NonNull StatsLogManager.LauncherEvent exitReason,
+            @NonNull SplitAnimationController animationController) {
+        StateAnimationConfig config = new StateAnimationConfig();
+        BaseState startState = stateManager.getState();
+        long duration = startState.getTransitionDuration(container.asContext(),
+                false /*isToState*/);
+        if (duration == 0) {
+            // Case where we're in contextual on workspace (NORMAL), which by default has 0
+            // transition duration
+            duration = DURATION_DEFAULT_SPLIT_DISMISS;
+        }
+        config.duration = duration;
+        AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+                startState, NORMAL, config);
+        AnimatorSet dismissAnim = animationController
+                .createPlaceholderDismissAnim(container, exitReason, duration);
+        stateAnim.play(dismissAnim);
+        stateManager.setCurrentAnimation(stateAnim, NORMAL);
+        stateAnim.start();
+    }
+
+    /**
      * Returns a IRemoteCallback which completes the provided list as a result
      */
     public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index c7c04ed..b583a4b 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -21,7 +21,6 @@
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
-import android.animation.AnimatorSet;
 import android.animation.TimeInterpolator;
 import android.content.Context;
 import android.graphics.Matrix;
@@ -34,18 +33,11 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.touch.AllAppsSwipeController;
-import com.android.quickstep.DeviceConfigWrapper;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * Controls an animation that can go beyond progress = 1, at which point resistance should be
@@ -57,10 +49,8 @@
 
     private enum RecentsResistanceParams {
         FROM_APP(0.75f, 0.5f, 1f, false),
-        FROM_APP_TO_ALL_APPS(1f, 0.6f, 0.8f, false),
         FROM_APP_TABLET(1f, 0.7f, 1f, true),
         FROM_APP_TABLET_GRID_ONLY(1f, 1f, 1f, true),
-        FROM_APP_TO_ALL_APPS_TABLET(1f, 0.5f, 0.5f, false),
         FROM_OVERVIEW(1f, 0.75f, 0.5f, false);
 
         RecentsResistanceParams(float scaleStartResist, float scaleMaxResist,
@@ -157,46 +147,10 @@
         RecentsParams params = new RecentsParams(context, recentsOrientedState, dp, scaleTarget,
                 scaleProperty, translationTarget, translationProperty);
         PendingAnimation resistAnim = createRecentsResistanceAnim(params);
-
-        // Apply All Apps animation during the resistance animation.
-        if (recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()) {
-            RecentsViewContainer container =
-                    recentsOrientedState.getContainerInterface().getCreatedContainer();
-            if (container != null) {
-                RecentsView recentsView = container.getOverviewPanel();
-                StateManager<LauncherState, StatefulActivity<LauncherState>> stateManager =
-                        recentsView.getStateManager();
-                if (stateManager.isInStableState(LauncherState.BACKGROUND_APP)
-                        && stateManager.isInTransition()) {
-
-                    // Calculate the resistance progress threshold where All Apps will trigger.
-                    float threshold = getAllAppsThreshold(context, recentsOrientedState, dp);
-
-                    StateAnimationConfig config = new StateAnimationConfig();
-                    AllAppsSwipeController.applyOverviewToAllAppsAnimConfig(dp, config, threshold);
-                    AnimatorSet allAppsAnimator = stateManager.createAnimationToNewWorkspace(
-                            LauncherState.ALL_APPS, config).getTarget();
-                    resistAnim.add(allAppsAnimator);
-                }
-            }
-        }
-
         AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
         return new AnimatorControllerWithResistance(normalController, resistanceController);
     }
 
-    private static float getAllAppsThreshold(Context context,
-            RecentsOrientedState recentsOrientedState, DeviceProfile dp) {
-        int transitionDragLength =
-                recentsOrientedState.getContainerInterface().getSwipeUpDestinationAndLength(
-                        dp, context, TEMP_RECT,
-                        recentsOrientedState.getOrientationHandler());
-        float dragLengthFactor = (float) dp.heightPx / transitionDragLength;
-        // -1s are because 0-1 is reserved for the normal transition.
-        float threshold = DeviceConfigWrapper.get().getAllAppsOverviewThreshold() / 100f;
-        return (threshold - 1) / (dragLengthFactor - 1);
-    }
-
     /**
      * Creates the resistance animation for {@link #createForRecents}, or can be used separately
      * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}.
@@ -305,17 +259,11 @@
             this.translationTarget = translationTarget;
             this.translationProperty = translationProperty;
             if (dp.isTablet) {
-                resistanceParams =
-                        recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()
-                                ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS_TABLET
-                                : enableGridOnlyOverview()
-                                        ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY
-                                        : RecentsResistanceParams.FROM_APP_TABLET;
+                resistanceParams = enableGridOnlyOverview()
+                        ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY
+                        : RecentsResistanceParams.FROM_APP_TABLET;
             } else {
-                resistanceParams =
-                        recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()
-                                ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS
-                                : RecentsResistanceParams.FROM_APP;
+                resistanceParams = RecentsResistanceParams.FROM_APP;
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index c3d74bb..7388d59 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -26,11 +26,11 @@
 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.common.split.SplitScreenConstants.SNAP_TO_50_50;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_NONE;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition;
+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;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.isPersistentSnapPosition;
 
 import android.content.Context;
 import android.content.Intent;
@@ -70,10 +70,11 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
+import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -188,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
@@ -232,14 +233,16 @@
      *
      * @param cuj Should be an integer from {@link Cuj} or -1 if no CUJ needs to be logged for jank
      *            monitoring
+     * @param callback Called after the app pair launch finishes animating, or null if no method is
+     *                 to be called
      */
-    public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
+    public void launchAppPair(AppPairIcon appPairIcon, int cuj,
+            @Nullable Consumer<Boolean> callback) {
         WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp();
         WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp();
         ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
         ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
-        mSplitSelectStateController.setLaunchingCuj(cuj);
-        InteractionJankMonitorWrapper.begin(appPairIcon, cuj);
+        mSplitSelectStateController.setLaunchingCuj(appPairIcon, cuj);
 
         mSplitSelectStateController.findLastActiveTasksAndRunCallback(
                 Arrays.asList(app1Key, app2Key),
@@ -273,12 +276,19 @@
                     mSplitSelectStateController.setLaunchingIconView(appPairIcon);
 
                     mSplitSelectStateController.launchSplitTasks(
-                            AppPairsController.convertRankToSnapPosition(app1.rank));
+                            AppPairsController.convertRankToSnapPosition(app1.rank), callback);
                 }
         );
     }
 
     /**
+     * Launches an app pair but does not specify a callback
+     */
+    public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
+        launchAppPair(appPairIcon, cuj, null);
+    }
+
+    /**
      * Returns an AppInfo associated with the app for the given ComponentKey, or null if no such
      * package exists in the AllAppsStore.
      */
diff --git a/quickstep/src/com/android/quickstep/util/AssistContentRequester.java b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
index 0ce54ad..2e3dee6 100644
--- a/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
+++ b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
@@ -81,7 +81,7 @@
             try {
                 mActivityTaskManager.requestAssistDataForTask(
                         new AssistDataReceiver(callback, this), taskId, mPackageName,
-                        mAttributionTag);
+                        mAttributionTag, false /* fetchStructure */);
             } catch (RemoteException e) {
                 Log.e(TAG, "Requesting assist content failed for task: " + taskId, e);
             }
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/BackAnimState.kt b/quickstep/src/com/android/quickstep/util/BackAnimState.kt
new file mode 100644
index 0000000..9009eaa
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BackAnimState.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.quickstep.util
+
+import android.animation.AnimatorSet
+import android.content.Context
+import com.android.launcher3.LauncherAnimationRunner.AnimationResult
+import com.android.launcher3.anim.AnimatorListeners.forEndCallback
+import com.android.launcher3.util.RunnableList
+
+/** Interface to represent animation for back to Launcher transition */
+interface BackAnimState {
+
+    fun addOnAnimCompleteCallback(r: Runnable)
+
+    fun applyToAnimationResult(result: AnimationResult, c: Context)
+
+    fun start()
+}
+
+class AnimatorBackState(private val springAnim: RectFSpringAnim?, private val anim: AnimatorSet?) :
+    BackAnimState {
+
+    override fun addOnAnimCompleteCallback(r: Runnable) {
+        val springAnimWait = RunnableList()
+        springAnim?.addAnimatorListener(forEndCallback(springAnimWait::executeAllAndDestroy))
+            ?: springAnimWait.executeAllAndDestroy()
+
+        val animWait = RunnableList()
+        anim?.addListener(
+            forEndCallback(Runnable { springAnimWait.add(animWait::executeAllAndDestroy) })
+        ) ?: springAnimWait.add(animWait::executeAllAndDestroy)
+        animWait.add(r)
+    }
+
+    override fun applyToAnimationResult(result: AnimationResult, c: Context) {
+        result.setAnimation(anim, c)
+    }
+
+    override fun start() {
+        anim?.start()
+    }
+}
+
+class AlreadyStartedBackAnimState(private val onEndCallback: RunnableList) : BackAnimState {
+
+    override fun addOnAnimCompleteCallback(r: Runnable) {
+        onEndCallback.add(r)
+    }
+
+    override fun applyToAnimationResult(result: AnimationResult, c: Context) {
+        addOnAnimCompleteCallback(result::onAnimationFinished)
+    }
+
+    override fun start() {}
+}
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ContextInitListener.java
similarity index 64%
rename from quickstep/src/com/android/quickstep/util/ActivityInitListener.java
rename to quickstep/src/com/android/quickstep/util/ContextInitListener.java
index 5efbb40..49f1463 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ContextInitListener.java
@@ -15,17 +15,17 @@
  */
 package com.android.quickstep.util;
 
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.util.ActivityTracker;
-import com.android.launcher3.util.ActivityTracker.SchedulerCallback;
+import com.android.launcher3.util.ContextTracker;
+import com.android.launcher3.util.ContextTracker.SchedulerCallback;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.function.BiPredicate;
 
-public class ActivityInitListener<T extends BaseActivity> implements
-        SchedulerCallback<T> {
+public class ContextInitListener<CONTEXT extends ActivityContext> implements
+        SchedulerCallback<CONTEXT> {
 
-    private BiPredicate<T, Boolean> mOnInitListener;
-    private final ActivityTracker<T> mActivityTracker;
+    private BiPredicate<CONTEXT, Boolean> mOnInitListener;
+    private final ContextTracker<CONTEXT> mContextTracker;
 
     private boolean mIsRegistered = false;
 
@@ -34,23 +34,23 @@
      *                       return true to continue receiving callbacks (ie. for if the activity is
      *                       recreated).
      */
-    public ActivityInitListener(BiPredicate<T, Boolean> onInitListener,
-            ActivityTracker<T> tracker) {
+    public ContextInitListener(BiPredicate<CONTEXT, Boolean> onInitListener,
+            ContextTracker<CONTEXT> tracker) {
         mOnInitListener = onInitListener;
-        mActivityTracker = tracker;
+        mContextTracker = tracker;
     }
 
     @Override
-    public final boolean init(T activity, boolean alreadyOnHome) {
+    public final boolean init(CONTEXT activity, boolean isHomeStarted) {
         if (!mIsRegistered) {
             // Don't receive any more updates
             return false;
         }
-        return handleInit(activity, alreadyOnHome);
+        return handleInit(activity, isHomeStarted);
     }
 
-    protected boolean handleInit(T activity, boolean alreadyOnHome) {
-        return mOnInitListener.test(activity, alreadyOnHome);
+    protected boolean handleInit(CONTEXT activity, boolean isHomeStarted) {
+        return mOnInitListener.test(activity, isHomeStarted);
     }
 
     /**
@@ -59,14 +59,14 @@
      */
     public void register(String reasonString) {
         mIsRegistered = true;
-        mActivityTracker.registerCallback(this, reasonString);
+        mContextTracker.registerCallback(this, reasonString);
     }
 
     /**
      * After calling this, we won't {@link #init} even when the activity is ready.
      */
     public void unregister(String reasonString) {
-        mActivityTracker.unregisterCallback(this, reasonString);
+        mContextTracker.unregisterCallback(this, reasonString);
         mIsRegistered = false;
         mOnInitListener = null;
     }
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt
new file mode 100644
index 0000000..286b77a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.content.Context
+import android.os.VibrationEffect
+import android.os.VibrationEffect.Composition
+import android.os.Vibrator
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.util.VibratorWrapper
+import com.android.quickstep.DeviceConfigWrapper.Companion.get
+import kotlin.math.pow
+
+/** Manages haptics relating to Contextual Search invocations. */
+class ContextualSearchHapticManager
+internal constructor(@ApplicationContext private val context: Context) : SafeCloseable {
+
+    private var searchEffect = createSearchEffect()
+    private var contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[context]
+
+    private fun createSearchEffect() =
+        if (
+            context
+                .getSystemService(Vibrator::class.java)!!
+                .areAllPrimitivesSupported(Composition.PRIMITIVE_TICK)
+        ) {
+            VibrationEffect.startComposition()
+                .addPrimitive(Composition.PRIMITIVE_TICK, 1f)
+                .compose()
+        } else {
+            // fallback for devices without composition support
+            VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK)
+        }
+
+    /** Indicates that search has been invoked. */
+    fun vibrateForSearch() {
+        searchEffect.let { VibratorWrapper.INSTANCE[context].vibrate(it) }
+    }
+
+    /** Indicates that search will be invoked if the current gesture is maintained. */
+    fun vibrateForSearchHint() {
+        val navbarConfig = get()
+        // Whether we should play the hint (ramp up) haptic
+        val shouldVibrate: Boolean =
+            if (
+                context
+                    .getSystemService(Vibrator::class.java)!!
+                    .areAllPrimitivesSupported(Composition.PRIMITIVE_LOW_TICK)
+            ) {
+                if (contextualSearchStateManager.shouldPlayHapticOverride.isPresent) {
+                    contextualSearchStateManager.shouldPlayHapticOverride.get()
+                } else {
+                    navbarConfig.enableSearchHapticHint
+                }
+            } else {
+                false
+            }
+
+        if (shouldVibrate) {
+            val startScale = navbarConfig.lpnhHapticHintStartScalePercent / 100f
+            val endScale = navbarConfig.lpnhHapticHintEndScalePercent / 100f
+            val scaleExponent = navbarConfig.lpnhHapticHintScaleExponent
+            val iterations = navbarConfig.lpnhHapticHintIterations
+            val delayMs = navbarConfig.lpnhHapticHintDelay
+            val composition = VibrationEffect.startComposition()
+            for (i in 0 until iterations) {
+                val t = i / (iterations - 1f)
+                val scale =
+                    ((1 - t) * startScale + t * endScale)
+                        .toDouble()
+                        .pow(scaleExponent.toDouble())
+                        .toFloat()
+                if (i == 0) {
+                    // Adds a delay before the ramp starts
+                    composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale, delayMs)
+                } else {
+                    composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale)
+                }
+            }
+            VibratorWrapper.INSTANCE[context].vibrate(composition.compose())
+        }
+    }
+
+    override fun close() {}
+
+    companion object {
+        @JvmField val INSTANCE = MainThreadInitializedObject { ContextualSearchHapticManager(it) }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
new file mode 100644
index 0000000..bd454c0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.app.contextualsearch.ContextualSearchManager
+import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_HOME
+import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH
+import android.content.Context
+import android.util.Log
+import com.android.internal.app.AssistUtils
+import com.android.launcher3.R
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME
+import com.android.launcher3.util.ResourceBasedOverride
+import com.android.quickstep.DeviceConfigWrapper
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.TopTaskTracker
+import com.android.systemui.shared.system.QuickStepContract
+
+/** Handles invocations and checks for Contextual Search. */
+open class ContextualSearchInvoker
+internal constructor(
+    protected val context: Context,
+    private val contextualSearchStateManager: ContextualSearchStateManager,
+    private val topTaskTracker: TopTaskTracker,
+    private val systemUiProxy: SystemUiProxy,
+    protected val statsLogManager: StatsLogManager,
+    private val contextualSearchHapticManager: ContextualSearchHapticManager,
+    private val contextualSearchManager: ContextualSearchManager?,
+) : ResourceBasedOverride {
+    constructor(
+        context: Context
+    ) : this(
+        context,
+        ContextualSearchStateManager.INSTANCE[context],
+        TopTaskTracker.INSTANCE[context],
+        SystemUiProxy.INSTANCE[context],
+        StatsLogManager.newInstance(context),
+        ContextualSearchHapticManager.INSTANCE[context],
+        context.getSystemService(ContextualSearchManager::class.java),
+    )
+
+    /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */
+    open fun getSysUiAssistOverrideInvocationTypes(): IntArray {
+        val overrideInvocationTypes = com.android.launcher3.util.IntArray()
+        if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
+            overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)
+        }
+        return overrideInvocationTypes.toArray()
+    }
+
+    /**
+     * @return `true` if the override was handled, i.e. an assist surface was shown or the request
+     *   should be ignored. `false` means the caller should start assist another way.
+     */
+    fun tryStartAssistOverride(invocationType: Int): Boolean {
+        if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) {
+            if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
+                // When Contextual Search is disabled, fall back to Assistant.
+                return false
+            }
+
+            val success = show(ENTRYPOINT_LONG_PRESS_HOME)
+            if (success) {
+                val runningPackage =
+                    TopTaskTracker.INSTANCE[context].getCachedTopTask(
+                            /* filterOnlyVisibleRecents */ true
+                        )
+                        .getPackageName()
+                statsLogManager
+                    .logger()
+                    .withPackageName(runningPackage)
+                    .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME)
+            }
+
+            // Regardless of success, do not fall back to other assistant.
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Invoke Contextual Search via ContextualSearchService if availability checks are successful
+     *
+     * @param entryPoint one of the ENTRY_POINT_* constants defined in this class
+     * @return true if invocation was successful, false otherwise
+     */
+    fun show(entryPoint: Int): Boolean {
+        return if (!runContextualSearchInvocationChecksAndLogFailures()) false
+        else invokeContextualSearchUnchecked(entryPoint)
+    }
+
+    /**
+     * Run availability checks and log errors to WW. If successful the caller is expected to call
+     * {@link invokeContextualSearchUnchecked}
+     *
+     * @return true if availability checks were successful, false otherwise.
+     */
+    fun runContextualSearchInvocationChecksAndLogFailures(): Boolean {
+        if (
+            contextualSearchManager == null ||
+                !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)
+        ) {
+            Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR)
+            return false
+        }
+        if (!contextualSearchStateManager.isContextualSearchSettingEnabled) {
+            Log.i(TAG, "Contextual Search invocation failed: setting disabled")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED)
+            return false
+        }
+        if (isNotificationShadeShowing()) {
+            Log.i(TAG, "Contextual Search invocation failed: notification shade")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE)
+            return false
+        }
+        if (isKeyguardShowing()) {
+            Log.i(TAG, "Contextual Search invocation attempted: keyguard")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD)
+            if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) {
+                Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed")
+                return false
+            } else if (!contextualSearchStateManager.supportsShowWhenLocked()) {
+                Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard")
+                return false
+            }
+        }
+        if (isInSplitscreen()) {
+            Log.i(TAG, "Contextual Search invocation attempted: splitscreen")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN)
+            if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) {
+                Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed")
+                return false
+            }
+        }
+        if (!contextualSearchStateManager.isContextualSearchIntentAvailable) {
+            Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE)
+            return false
+        }
+
+        return true
+    }
+
+    /**
+     * Invoke Contextual Search via ContextualSearchService and do haptic
+     *
+     * @param entryPoint Entry point identifier, passed to ContextualSearchService.
+     * @return true if invocation was successful, false otherwise
+     */
+    fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean {
+        return invokeContextualSearchUnchecked(entryPoint, withHaptic = true)
+    }
+
+    private fun invokeContextualSearchUnchecked(
+        entryPoint: Int,
+        withHaptic: Boolean = false,
+    ): Boolean {
+        if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) {
+            contextualSearchHapticManager.vibrateForSearch()
+        }
+        if (contextualSearchManager == null) {
+            return false
+        }
+        contextualSearchManager.startContextualSearch(entryPoint)
+        return true
+    }
+
+    private fun isInSplitscreen(): Boolean {
+        return topTaskTracker.getRunningSplitTaskIds().isNotEmpty()
+    }
+
+    private fun isNotificationShadeShowing(): Boolean {
+        return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L
+    }
+
+    private fun isKeyguardShowing(): Boolean {
+        return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L
+    }
+
+    companion object {
+        private const val TAG = "ContextualSearchInvoker"
+        const val SHADE_EXPANDED_SYSUI_FLAGS =
+            QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or
+                QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+        const val KEYGUARD_SHOWING_SYSUI_FLAGS =
+            (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or
+                QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or
+                QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED)
+
+        @JvmStatic
+        fun newInstance(context: Context): ContextualSearchInvoker {
+            return ResourceBasedOverride.Overrides.getObject(
+                ContextualSearchInvoker::class.java,
+                context,
+                R.string.contextual_search_invoker_class,
+            )
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
new file mode 100644
index 0000000..083f192
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.app.contextualsearch.ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH;
+import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_SYSTEM_ACTION;
+import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.R;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.util.EventLogArray;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
+import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TopTaskTracker;
+
+import java.io.PrintWriter;
+import java.util.Optional;
+
+/** Long-lived class to manage Contextual Search states like the user setting and availability. */
+public class ContextualSearchStateManager implements ResourceBasedOverride, SafeCloseable {
+
+    public static final MainThreadInitializedObject<ContextualSearchStateManager> INSTANCE =
+            forOverride(ContextualSearchStateManager.class,
+                    R.string.contextual_search_state_manager_class);
+
+    private static final String TAG = "ContextualSearchStMgr";
+    private static final int MAX_DEBUG_EVENT_SIZE = 20;
+    private static final Uri SEARCH_ALL_ENTRYPOINTS_ENABLED_URI =
+            Settings.Secure.getUriFor(Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED);
+
+    private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi;
+    private final SimpleBroadcastReceiver mContextualSearchPackageReceiver =
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, (unused) -> requestUpdateProperties());
+    private final SettingsCache.OnChangeListener mContextualSearchSettingChangedListener =
+            this::onContextualSearchSettingChanged;
+    protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE);
+
+    @Nullable private SettingsCache mSettingsCache;
+    // Cached value whether the ContextualSearch intent filter matched any enabled components.
+    private boolean mIsContextualSearchIntentAvailable;
+    private boolean mIsContextualSearchSettingEnabled;
+
+    protected Context mContext;
+    protected String mContextualSearchPackage;
+
+    public ContextualSearchStateManager() {}
+
+    public ContextualSearchStateManager(Context context) {
+        mContext = context;
+        mContextualSearchPackage = mContext.getResources().getString(
+                com.android.internal.R.string.config_defaultContextualSearchPackageName);
+
+        if (areAllContextualSearchFlagsDisabled()
+                || !context.getPackageManager().hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
+            // If we had previously registered a SystemAction which is no longer valid, we need to
+            // unregister it here.
+            unregisterSearchScreenSystemAction();
+            // Don't listen for stuff we aren't gonna use.
+            return;
+        }
+
+        requestUpdateProperties();
+        registerSearchScreenSystemAction();
+        mContextualSearchPackageReceiver.registerPkgActions(
+                context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED,
+                Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED);
+
+        mSettingsCache = SettingsCache.INSTANCE.get(context);
+        mSettingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
+                mContextualSearchSettingChangedListener);
+        onContextualSearchSettingChanged(
+                mSettingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI));
+        SystemUiProxy.INSTANCE.get(mContext).addOnStateChangeListener(mSysUiStateChangeListener);
+    }
+
+    /** Return {@code true} if the Settings toggle is enabled. */
+    public final boolean isContextualSearchSettingEnabled() {
+        return mIsContextualSearchSettingEnabled;
+    }
+
+    private void onContextualSearchSettingChanged(boolean isEnabled) {
+        mIsContextualSearchSettingEnabled = isEnabled;
+    }
+
+    /** Whether search supports showing on the lockscreen. */
+    protected boolean supportsShowWhenLocked() {
+        return false;
+    }
+
+    /** Whether ContextualSearchService invocation path is available. */
+    @VisibleForTesting
+    protected final boolean isContextualSearchIntentAvailable() {
+        return mIsContextualSearchIntentAvailable;
+    }
+
+    /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */
+    public Optional<Long> getLPNHDurationMillis() {
+        return Optional.empty();
+    }
+
+    /**
+     * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
+     */
+    public Optional<Float> getLPNHCustomSlopMultiplier() {
+        return Optional.empty();
+    }
+
+    /** Get the Launcher overridden long press home duration to trigger Assistant. */
+    public Optional<Long> getLPHDurationMillis() {
+        return Optional.empty();
+    }
+
+    /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
+    public Optional<Float> getLPHCustomSlopMultiplier() {
+        return Optional.empty();
+    }
+
+    /** Get the long press duration data source. */
+    public int getDurationDataSource() {
+        return 0;
+    }
+
+    /** Get the long press touch slop multiplier data source. */
+    public int getSlopDataSource() {
+        return 0;
+    }
+
+    /**
+     * Get the User group based on the behavior to trigger Assistant.
+     */
+    public Optional<Integer> getLPUserGroup() {
+        return Optional.empty();
+    }
+
+    /** Get the haptic bit overridden by AGSA. */
+    public Optional<Boolean> getShouldPlayHapticOverride() {
+        return Optional.empty();
+    }
+
+    protected boolean isInvocationAllowedOnKeyguard() {
+        return false;
+    }
+
+    protected boolean isInvocationAllowedInSplitscreen() {
+        return true;
+    }
+
+    @CallSuper
+    protected boolean areAllContextualSearchFlagsDisabled() {
+        return !DeviceConfigWrapper.get().getEnableLongPressNavHandle();
+    }
+
+    @CallSuper
+    protected void requestUpdateProperties() {
+        UI_HELPER_EXECUTOR.execute(() -> {
+            // Check that Contextual Search intent filters are enabled.
+            Intent csIntent = new Intent(ACTION_LAUNCH_CONTEXTUAL_SEARCH).setPackage(
+                    mContextualSearchPackage);
+            mIsContextualSearchIntentAvailable =
+                    !mContext.getPackageManager().queryIntentActivities(csIntent,
+                            PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE).isEmpty();
+
+            addEventLog("Updated isContextualSearchIntentAvailable",
+                    mIsContextualSearchIntentAvailable);
+        });
+    }
+
+    protected final void updateOverridesToSysUi() {
+        // LPH commit haptic is always enabled
+        SystemUiProxy.INSTANCE.get(mContext).setOverrideHomeButtonLongPress(
+                getLPHDurationMillis().orElse(0L), getLPHCustomSlopMultiplier().orElse(0f), true);
+        Log.i(TAG, "Sent LPH override to sysui: " + getLPHDurationMillis().orElse(0L) + ";"
+                + getLPHCustomSlopMultiplier().orElse(0f));
+    }
+
+    private void registerSearchScreenSystemAction() {
+        PendingIntent searchScreenPendingIntent = new PendingIntent(new IIntentSender.Stub() {
+            @Override
+            public void send(int i, Intent intent, String s, IBinder iBinder,
+                    IIntentReceiver iIntentReceiver, String s1, Bundle bundle)
+                    throws RemoteException {
+                // Delayed slightly to minimize chance of capturing the System Actions dialog.
+                UI_HELPER_EXECUTOR.getHandler().postDelayed(
+                        () -> {
+                            boolean contextualSearchInvoked =
+                                    ContextualSearchInvoker.newInstance(mContext).show(
+                                            ENTRYPOINT_SYSTEM_ACTION);
+                            if (contextualSearchInvoked) {
+                                String runningPackage =
+                                        TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask(
+                                                /* filterOnlyVisibleRecents */
+                                                true).getPackageName();
+                                StatsLogManager.newInstance(mContext).logger()
+                                        .withPackageName(runningPackage)
+                                        .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION);
+                            }
+                        }, 200);
+            }
+        });
+
+        mContext.getSystemService(AccessibilityManager.class).registerSystemAction(new RemoteAction(
+                        Icon.createWithResource(mContext, R.drawable.ic_allapps_search),
+                        mContext.getString(R.string.search_gesture_feature_title),
+                        mContext.getString(R.string.search_gesture_feature_title),
+                        searchScreenPendingIntent),
+                SYSTEM_ACTION_ID_SEARCH_SCREEN);
+    }
+
+    private void unregisterSearchScreenSystemAction() {
+        mContext.getSystemService(AccessibilityManager.class).unregisterSystemAction(
+                SYSTEM_ACTION_ID_SEARCH_SCREEN);
+    }
+
+    /** Dump states. */
+    public final void dump(String prefix, PrintWriter writer) {
+        synchronized (mEventLogArray) {
+            mEventLogArray.dump(prefix, writer);
+        }
+    }
+
+    @Override
+    public void close() {
+        mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext);
+        unregisterSearchScreenSystemAction();
+
+        if (mSettingsCache != null) {
+            mSettingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
+                    mContextualSearchSettingChangedListener);
+        }
+        SystemUiProxy.INSTANCE.get(mContext).removeOnStateChangeListener(mSysUiStateChangeListener);
+    }
+
+    protected final void addEventLog(String event) {
+        synchronized (mEventLogArray) {
+            mEventLogArray.addLog(event);
+        }
+    }
+
+    protected final void addEventLog(String event, boolean extras) {
+        synchronized (mEventLogArray) {
+            mEventLogArray.addLog(event, extras);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index a727aa2..fc4fc4d 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -50,6 +50,11 @@
 
     @Override
     public boolean hasMultipleTasks() {
+        return tasks.size() > 1;
+    }
+
+    @Override
+    public boolean supportsMultipleTasks() {
         return true;
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
index d36dc7e..2dd727e 100644
--- a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
+++ b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
@@ -26,19 +26,21 @@
 import androidx.annotation.WorkerThread
 import com.android.launcher3.BuildConfig
 import com.android.launcher3.util.Executors
+import java.util.concurrent.CopyOnWriteArrayList
 
 /** Utility class to manage a set of device configurations */
 class DeviceConfigHelper<ConfigType>(private val factory: (PropReader) -> ConfigType) {
 
     var config: ConfigType
         private set
+
     private val allKeys: Set<String>
     private val propertiesListener = OnPropertiesChangedListener { onDevicePropsChanges(it) }
     private val sharedPrefChangeListener = OnSharedPreferenceChangeListener { _, _ ->
         recreateConfig()
     }
 
-    private val changeListeners = mutableListOf<Runnable>()
+    private val changeListeners = CopyOnWriteArrayList<Runnable>()
 
     init {
         // Initialize the default config once.
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/InputConsumerProxy.java b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
index cb44a1a..fcf9ab1 100644
--- a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
@@ -108,19 +108,28 @@
             return false;
         }
 
+        final SimpleOrientationTouchTransformer touchTransformer =
+                SimpleOrientationTouchTransformer.INSTANCE.get(mContext);
+        final int viewRotation = mRotationSupplier.get();
+        final boolean needTransform = viewRotation != ev.getSurfaceRotation();
         if (action == ACTION_DOWN) {
             mTouchInProgress = true;
+            if (needTransform) {
+                touchTransformer.updateTouchingOrientation(viewRotation);
+            }
             initInputConsumerIfNeeded(/* isFromTouchDown= */ true);
         } else if (action == ACTION_CANCEL || action == ACTION_UP) {
             // Finish any pending actions
             mTouchInProgress = false;
+            touchTransformer.clearTouchingOrientation();
             if (mDestroyPending) {
                 destroy();
             }
         }
         if (mInputConsumer != null) {
-            SimpleOrientationTouchTransformer.INSTANCE.get(mContext).transform(ev,
-                    mRotationSupplier.get());
+            if (needTransform) {
+                touchTransformer.transform(ev, viewRotation);
+            }
             mInputConsumer.onMotionEvent(ev);
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
index 26668c8..4c26761 100644
--- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
+++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
@@ -31,7 +31,6 @@
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.HorizontalInsettableView;
 import com.android.quickstep.SystemUiProxy;
@@ -80,17 +79,12 @@
             @UnfoldMain RotationChangeProvider rotationChangeProvider) {
         mLauncher = launcher;
 
-        if (FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
-            mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider(
-                    unfoldTransitionProgressProvider, launcher.getMainThreadHandler());
-            mPreemptiveProgressProvider.init();
+        mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider(
+                unfoldTransitionProgressProvider, launcher.getMainThreadHandler());
+        mPreemptiveProgressProvider.init();
 
-            mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
-                    mPreemptiveProgressProvider);
-        } else {
-            mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
-                    unfoldTransitionProgressProvider);
-        }
+        mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
+                mPreemptiveProgressProvider);
 
         unfoldTransitionProgressProvider.addCallback(mExternalTransitionStatusProvider);
         unfoldTransitionProgressProvider.addCallback(
@@ -169,10 +163,6 @@
 
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
-        if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
-            return;
-        }
-
         if (mIsTablet != null && dp.isTablet != mIsTablet) {
             // We should preemptively start the animation only if:
             // - We changed to the unfolded screen
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index ec1eeb1..a8460c9 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -23,11 +23,13 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.NavigationMode;
-import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 
 public class LayoutUtils {
 
+    private static final float SQUARE_ASPECT_RATIO_TOLERANCE = 0.05f;
+
     /**
      * The height for the swipe up motion
      */
@@ -39,11 +41,14 @@
         return swipeHeight;
     }
 
-    public static int getShelfTrackingDistance(Context context, DeviceProfile dp,
-            RecentsPagedOrientationHandler orientationHandler) {
+    public static int getShelfTrackingDistance(
+            Context context,
+            DeviceProfile dp,
+            RecentsPagedOrientationHandler orientationHandler,
+            BaseContainerInterface<?, ?> baseContainerInterface) {
         // Track the bottom of the window.
         Rect taskSize = new Rect();
-        LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize,
+        baseContainerInterface.calculateTaskSize(context, dp, taskSize,
                 orientationHandler);
         return orientationHandler.getDistanceToBottomOfRect(dp, taskSize);
     }
@@ -61,4 +66,13 @@
             }
         }
     }
+
+    /**
+     * Returns true iff the device's aspect ratio is within
+     * {@link LayoutUtils#SQUARE_ASPECT_RATIO_TOLERANCE} of 1:1
+     */
+    public static boolean isAspectRatioSquare(float aspectRatio) {
+        return Float.compare(aspectRatio, 1f - SQUARE_ASPECT_RATIO_TOLERANCE) >= 0
+                && Float.compare(aspectRatio, 1f + SQUARE_ASPECT_RATIO_TOLERANCE) <= 0;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index b8bc828..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;
@@ -66,6 +68,7 @@
     private Float mPreviousVelocity = null;
 
     private OnMotionPauseListener mOnMotionPauseListener;
+    private boolean mIsTrackpadGesture;
     private boolean mIsPaused;
     // Bias more for the first pause to make it feel extra responsive.
     private boolean mHasEverBeenPaused;
@@ -94,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);
         });
@@ -115,13 +118,16 @@
         mOnMotionPauseListener = listener;
     }
 
+    public void setIsTrackpadGesture(boolean isTrackpadGesture) {
+        mIsTrackpadGesture = isTrackpadGesture;
+    }
+
     /**
      * @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);
         }
@@ -179,11 +185,14 @@
                     // We want to be more aggressive about detecting the first pause to ensure it
                     // feels as responsive as possible; getting two very slow speeds back to back
                     // takes too long, so also check for a rapid deceleration.
-                    boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR;
-                    isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
+                    boolean isRapidDeceleration =
+                            speed < previousSpeed * getRapidDecelerationFactor();
+                    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) {
@@ -192,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;
@@ -209,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) {
@@ -239,20 +245,20 @@
         }
     }
 
-    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() {
         mVelocityProvider.clear();
         mPreviousVelocity = null;
         setOnMotionPauseListener(null);
+        mIsTrackpadGesture = false;
         mIsPaused = mHasEverBeenPaused = false;
         mSlowStartTime = 0;
         mForcePauseTimeout.cancelAlarm();
@@ -262,6 +268,13 @@
         return mIsPaused;
     }
 
+    private float getRapidDecelerationFactor() {
+        return mIsTrackpadGesture ? Float.parseFloat(
+                Utilities.getSystemProperty("trackpad_in_app_swipe_up_deceleration_factor",
+                        String.valueOf(RAPID_DECELERATION_FACTOR_TRACKPAD)))
+                : RAPID_DECELERATION_FACTOR;
+    }
+
     public interface OnMotionPauseListener {
         /** Called only the first time motion pause is detected. */
         void onMotionPauseDetected();
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index cfe5b2c..70ef47c 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -106,7 +106,8 @@
                         return;
                     }
                     mShouldIncreaseCount = toState == HINT_STATE
-                            && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE;
+                            && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE
+                            && launcher.isCanShowAllAppsEducationView();
                 }
 
                 @Override
diff --git a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
index 0b05c2e..63fe017 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.util;
 
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA;
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
@@ -33,8 +34,10 @@
 
     public static final int INDEX_RECENTS_FADE_ANIM = AtomicAnimationFactory.NEXT_INDEX + 0;
     public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = AtomicAnimationFactory.NEXT_INDEX + 1;
+    public static final int INDEX_RECENTS_ATTACHED_ALPHA_ANIM =
+            AtomicAnimationFactory.NEXT_INDEX + 2;
 
-    private static final int MY_ANIM_COUNT = 2;
+    private static final int MY_ANIM_COUNT = 3;
 
     protected final CONTAINER mContainer;
 
@@ -50,6 +53,7 @@
                 ObjectAnimator alpha = ObjectAnimator.ofFloat(mContainer.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
                 return alpha;
+            case INDEX_RECENTS_ATTACHED_ALPHA_ANIM:
             case INDEX_RECENTS_TRANSLATE_X_ANIM: {
                 RecentsView rv = mContainer.getOverviewPanel();
                 return new SpringAnimationBuilder(mContainer)
@@ -57,7 +61,8 @@
                         .setDampingRatio(0.8f)
                         .setStiffness(250)
                         .setValues(values)
-                        .build(rv, ADJACENT_PAGE_HORIZONTAL_OFFSET);
+                        .build(rv, index == INDEX_RECENTS_ATTACHED_ALPHA_ANIM
+                                ? RUNNING_TASK_ATTACH_ALPHA : ADJACENT_PAGE_HORIZONTAL_OFFSET);
             }
             default:
                 return super.createStateElementAnimation(index, values);
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
new file mode 100644
index 0000000..c3b072d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
+import com.android.quickstep.RecentsAnimationController
+import com.android.quickstep.views.DesktopTaskView
+import com.android.quickstep.views.TaskView
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.ThumbnailData
+
+/**
+ * Helper class for [com.android.quickstep.views.RecentsView]. This util class contains refactored
+ * and extracted functions from RecentsView to facilitate the implementation of unit tests.
+ */
+class RecentsViewUtils {
+    /** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */
+    fun screenshotTasks(
+        taskView: TaskView,
+        recentsAnimationController: RecentsAnimationController,
+    ): Map<Int, ThumbnailData> =
+        taskView.taskContainers.associate {
+            it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id)
+        }
+
+    /**
+     * Sorts task groups to move desktop tasks to the end of the list.
+     *
+     * @param tasks List of group tasks to be sorted.
+     * @return Sorted list of GroupTasks to be used in the RecentsView.
+     */
+    fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
+        val (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+        return otherTasks + desktopTasks
+    }
+
+    /** Returns the expected index of the focus task. */
+    fun getFocusedTaskIndex(taskGroups: List<GroupTask>): Int {
+        // The focused task index is placed after the desktop tasks views.
+        return if (enableLargeDesktopWindowingTile()) {
+            taskGroups.count { it.taskViewType == TaskViewType.DESKTOP }
+        } else {
+            0
+        }
+    }
+
+    /** Counts [TaskView]s that are [DesktopTaskView] instances. */
+    fun getDesktopTaskViewCount(taskViews: Iterable<TaskView>): Int =
+        taskViews.count { it is DesktopTaskView }
+
+    /** Returns a list of all large TaskView Ids from [TaskView]s */
+    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: 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? =
+        taskViews.lastOrNull { it.isLargeTile }
+
+    /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */
+    fun getFirstTaskViewInCarousel(
+        nonRunningTaskCarouselHidden: Boolean,
+        taskViews: Iterable<TaskView>,
+        runningTaskView: TaskView?,
+    ): TaskView? =
+        taskViews.firstOrNull {
+            it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
+        }
+
+    /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */
+    fun getLastTaskViewInCarousel(
+        nonRunningTaskCarouselHidden: Boolean,
+        taskViews: Iterable<TaskView>,
+        runningTaskView: TaskView?,
+    ): TaskView? =
+        taskViews.lastOrNull {
+            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)
+
+    /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */
+    fun applyAttachAlpha(
+        taskViews: Iterable<TaskView>,
+        runningTaskView: TaskView?,
+        runningTaskAttachAlpha: Float,
+        nonRunningTaskCarouselHidden: Boolean,
+    ) {
+        taskViews.forEach { taskView ->
+            taskView.attachAlpha =
+                if (taskView == runningTaskView) {
+                    runningTaskAttachAlpha
+                } else {
+                    if (taskView.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden))
+                        1f
+                    else 0f
+                }
+        }
+    }
+
+    fun TaskView.isVisibleInCarousel(
+        runningTaskView: TaskView?,
+        nonRunningTaskCarouselHidden: Boolean,
+    ): Boolean =
+        if (!nonRunningTaskCarouselHidden) true
+        else getCarouselType() == runningTaskView.getCarouselType()
+
+    /** 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 TaskViewCarousel {
+        FULL_SCREEN,
+        DESKTOP,
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
index f547a7fb..f719bed 100644
--- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
+++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
@@ -16,15 +16,20 @@
 
 package com.android.quickstep.util
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
 import android.graphics.Matrix
 import android.graphics.Path
 import android.graphics.RectF
+import android.util.Log
 import android.view.View
 import android.view.animation.PathInterpolator
 import androidx.core.graphics.transform
+import com.android.app.animation.Animations
 import com.android.app.animation.Interpolators
 import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY
 import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE
 import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY
@@ -39,14 +44,16 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher
 import com.android.quickstep.views.RecentsView
 
+const val TAG = "ScalingWorkspaceRevealAnim"
+
 /**
  * Creates an animation where the workspace and hotseat fade in while revealing from the center of
  * the screen outwards radially. This is used in conjunction with the swipe up to home animation.
  */
 class ScalingWorkspaceRevealAnim(
-    launcher: QuickstepLauncher,
+    private val launcher: QuickstepLauncher,
     siblingAnimation: RectFSpringAnim?,
-    windowTargetRect: RectF?
+    windowTargetRect: RectF?,
 ) {
     companion object {
         private const val FADE_DURATION_MS = 200L
@@ -60,7 +67,8 @@
          * Custom interpolator for both the home and wallpaper scaling. Necessary because EMPHASIZED
          * is too aggressive, but EMPHASIZED_DECELERATE is too soft.
          */
-        private val SCALE_INTERPOLATOR =
+        @JvmField
+        val SCALE_INTERPOLATOR =
             PathInterpolator(
                 Path().apply {
                     moveTo(0f, 0f)
@@ -86,25 +94,40 @@
         launcher.workspace.stateTransitionAnimation.setScrim(
             PropertySetter.NO_ANIM_PROPERTY_SETTER,
             LauncherState.BACKGROUND_APP,
-            setupConfig
+            setupConfig,
         )
 
         val workspace = launcher.workspace
         val hotseat = launcher.hotseat
 
+        var fromSize =
+            if (Flags.coordinateWorkspaceScale()) {
+                // Interrupt the current animation, if any.
+                Animations.cancelOngoingAnimation(workspace)
+                Animations.cancelOngoingAnimation(hotseat)
+
+                if (workspace.scaleX != MAX_SIZE) {
+                    workspace.scaleX
+                } else {
+                    MIN_SIZE
+                }
+            } else {
+                MIN_SIZE
+            }
+
         // Scale the Workspace and Hotseat around the same pivot.
         workspace.setPivotToScaleWithSelf(hotseat)
         animation.addFloat(
             workspace,
             WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE],
-            MIN_SIZE,
+            fromSize,
             MAX_SIZE,
             SCALE_INTERPOLATOR,
         )
         animation.addFloat(
             hotseat,
             HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE],
-            MIN_SIZE,
+            fromSize,
             MAX_SIZE,
             SCALE_INTERPOLATOR,
         )
@@ -116,13 +139,13 @@
         animation.setViewAlpha(
             workspace,
             MAX_ALPHA,
-            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp)
+            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp),
         )
         hotseat.alpha = MIN_ALPHA
         animation.setViewAlpha(
             hotseat,
             MAX_ALPHA,
-            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp)
+            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp),
         )
 
         val transitionConfig = StateAnimationConfig()
@@ -137,7 +160,7 @@
         launcher.workspace.stateTransitionAnimation.setScrim(
             animation,
             LauncherState.NORMAL,
-            transitionConfig
+            transitionConfig,
         )
 
         // To avoid awkward jumps in icon position, we want the sibling animation to always be
@@ -164,7 +187,7 @@
                         1 / workspace.scaleX,
                         1 / workspace.scaleY,
                         transformed.centerX(),
-                        transformed.centerY()
+                        transformed.centerY(),
                     )
                 }
             )
@@ -179,10 +202,36 @@
         workspace.setLayerType(View.LAYER_TYPE_HARDWARE, null)
         hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null)
         animation.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationCancel(animation: Animator) {
+                    super.onAnimationCancel(animation)
+                    Log.d(TAG, "onAnimationCancel")
+                }
+
+                override fun onAnimationPause(animation: Animator) {
+                    super.onAnimationPause(animation)
+                    Log.d(TAG, "onAnimationPause")
+                }
+            }
+        )
+        animation.addListener(
             AnimatorListeners.forEndCallback(
                 Runnable {
+                    // The workspace might stay at a transparent state when the animation is
+                    // cancelled, and the alpha will not be recovered (this doesn't apply to scales
+                    // somehow). Resetting the alpha for the workspace here.
+                    workspace.alpha = 1.0F
+
                     workspace.setLayerType(View.LAYER_TYPE_NONE, null)
                     hotseat.setLayerType(View.LAYER_TYPE_NONE, null)
+
+                    if (Flags.coordinateWorkspaceScale()) {
+                        // Reset the cached animations.
+                        Animations.setOngoingAnimation(workspace, animation = null)
+                        Animations.setOngoingAnimation(hotseat, animation = null)
+                    }
+
+                    Log.d(TAG, "alpha of workspace at the end of animation: ${workspace.alpha}")
                 }
             )
         )
@@ -193,6 +242,14 @@
     }
 
     fun start() {
-        getAnimators().start()
+        val animators = getAnimators()
+        if (Flags.coordinateWorkspaceScale()) {
+            // Make sure to cache the current animation, so it can be properly interrupted.
+            // TODO(b/367591368): ideally these animations would be refactored to be controlled
+            //  centrally so each instances doesn't need to care about this coordination.
+            Animations.setOngoingAnimation(launcher.workspace, animators)
+            Animations.setOngoingAnimation(launcher.hotseat, animators)
+        }
+        animators.start()
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index e31a828..f708f4b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -42,6 +42,8 @@
 import android.window.TransitionInfo.Change
 import android.window.WindowContainerToken
 import androidx.annotation.VisibleForTesting
+import androidx.core.util.component1
+import androidx.core.util.component2
 import com.android.app.animation.Interpolators
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.Flags.enableOverviewIconMenu
@@ -91,7 +93,8 @@
             val iconDrawable: Drawable,
             val fadeWithThumbnail: Boolean,
             val isStagedTask: Boolean,
-            val iconView: View?
+            val iconView: View?,
+            val contentDescription: CharSequence?
         )
     }
 
@@ -112,7 +115,8 @@
                 splitSelectSource.drawable,
                 fadeWithThumbnail = false,
                 isStagedTask = true,
-                iconView = null
+                iconView = null,
+                splitSelectSource.itemInfo.contentDescription
             )
         } else if (splitSelectStateController.isDismissingFromSplitPair) {
             // Initiating split from overview, but on a split pair
@@ -126,7 +130,8 @@
                         drawable,
                         fadeWithThumbnail = true,
                         isStagedTask = true,
-                        iconView = container.iconView.asView()
+                        iconView = container.iconView.asView(),
+                        container.task.titleDescription
                     )
                 }
             }
@@ -145,7 +150,8 @@
                     drawable,
                     fadeWithThumbnail = true,
                     isStagedTask = true,
-                    iconView = it.iconView.asView()
+                    iconView = it.iconView.asView(),
+                    it.task.titleDescription
                 )
             }
         }
@@ -187,7 +193,6 @@
     ) {
         val snapshot = taskContainer.snapshotView
         val iconView: View = taskContainer.iconView.asView()
-        // TODO(334826842): Switch to splash state in TaskThumbnailView
         if (!enableRefactorTaskThumbnail()) {
             val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated
             builder.add(
@@ -198,6 +203,15 @@
                 )
             )
             thumbnailViewDeprecated.setShowSplashForSplitSelection(true)
+        } else {
+            builder.add(
+                ValueAnimator.ofFloat(0f, 1f).apply {
+                    addUpdateListener {
+                        taskContainer.taskContainerData.thumbnailSplashProgress.value =
+                            it.animatedFraction
+                    }
+                }
+            )
         }
         // With the new `IconAppChipView`, we always want to keep the chip pinned to the
         // top left of the task / thumbnail.
@@ -213,10 +227,22 @@
                 ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f)
             )
         }
+
+        val splitBoundsConfig =
+            (taskContainer.taskView as? GroupedTaskView)?.splitBoundsConfig ?: return
+        val (primarySnapshotViewSize, secondarySnapshotViewSize) =
+            taskContainer.taskView.pagedOrientationHandler.getGroupedTaskViewSizes(
+                deviceProfile,
+                splitBoundsConfig,
+                taskViewWidth,
+                taskViewHeight,
+            )
+        val snapshotViewSize =
+            if (isPrimaryTaskSplitting) secondarySnapshotViewSize else primarySnapshotViewSize
         if (deviceProfile.isLeftRightSplit) {
             // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
-            val centerThumbnailTranslationX: Float = (taskViewWidth - snapshot.width) / 2f
-            val finalScaleX: Float = taskViewWidth.toFloat() / snapshot.width
+            val centerThumbnailTranslationX: Float = (taskViewWidth - snapshotViewSize.x) / 2f
+            val finalScaleX: Float = taskViewWidth.toFloat() / snapshotViewSize.x
             builder.add(
                 ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, centerThumbnailTranslationX)
             )
@@ -250,13 +276,13 @@
             //  thumbnail needs to take that into account. We should migrate to only using
             //  translations otherwise this asymmetry causes problems..
             if (isPrimaryTaskSplitting) {
-                centerThumbnailTranslationY = (thumbnailSize - snapshot.height) / 2f
+                centerThumbnailTranslationY = (thumbnailSize - snapshotViewSize.y) / 2f
                 centerThumbnailTranslationY +=
                     deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
             } else {
-                centerThumbnailTranslationY = (thumbnailSize - snapshot.height) / 2f
+                centerThumbnailTranslationY = (thumbnailSize - snapshotViewSize.y) / 2f
             }
-            val finalScaleY: Float = thumbnailSize.toFloat() / snapshot.height
+            val finalScaleY: Float = thumbnailSize.toFloat() / snapshotViewSize.y
             builder.add(
                 ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, centerThumbnailTranslationY)
             )
@@ -719,16 +745,19 @@
         val mainRootCandidate = splitRoots.first
         // Will contain changes (1) and (2) in diagram above
         val leafRoots: List<Change> = splitRoots.second
+        // Don't rely on DP.isLeftRightSplit because if launcher is portrait apps could still
+        // launch in landscape if system auto-rotate is enabled and phone is held horizontally
+        val isLeftRightSplit = leafRoots.all { it.endAbsBounds.top == 0 }
 
         // Find the place where our left/top app window meets the divider (used for the
         // launcher side animation)
         val leftTopApp =
             leafRoots.single { change ->
-                (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
-                    (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
+                (isLeftRightSplit && change.endAbsBounds.left == 0) ||
+                    (!isLeftRightSplit && change.endAbsBounds.top == 0)
             }
         val dividerPos =
-            if (dp.isLeftRightSplit) leftTopApp.endAbsBounds.right
+            if (isLeftRightSplit) leftTopApp.endAbsBounds.right
             else leftTopApp.endAbsBounds.bottom
 
         // Create a new floating view in Launcher, positioned above the launching icon
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/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index d58cb91..d982e81 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -17,14 +17,13 @@
 package com.android.quickstep.util
 
 import android.util.Log
-import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.TransitionInfo
 import android.window.TransitionInfo.Change
 import android.window.TransitionInfo.FLAG_FIRST_CUSTOM
 import com.android.launcher3.util.SplitConfigurationOptions
-import com.android.wm.shell.util.SplitBounds
+import com.android.wm.shell.shared.split.SplitBounds
 import java.lang.IllegalStateException
 
 class SplitScreenUtils {
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index d906bb3..ea582c4 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP;
@@ -35,8 +34,8 @@
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT;
 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.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -53,16 +52,13 @@
 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.SystemClock;
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Pair;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
+import android.view.View;
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.RemoteTransition;
 import android.window.RemoteTransitionStub;
@@ -81,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;
@@ -94,18 +91,15 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SplitSelectionListener;
 import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.SplitInstructionsView;
-import com.android.systemui.animation.RemoteAnimationRunnerCompat;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
-import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
+import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
 import com.android.wm.shell.splitscreen.ISplitSelectListener;
 
 import java.io.PrintWriter;
@@ -122,7 +116,6 @@
     private static final String TAG = "SplitSelectStateCtor";
 
     private RecentsViewContainer mContainer;
-    private final Handler mHandler;
     private final RecentsModel mRecentTasksModel;
     @Nullable
     private Runnable mActivityBackCallback;
@@ -155,9 +148,10 @@
 
     /**
      * Should be a constant from {@link com.android.internal.jank.Cuj} or -1, does not need to be
-     * set for all launches.
+     * set for all launches. Used in conjunction with {@link #mLaunchingViewCuj} below.
      */
     private int mLaunchCuj = -1;
+    private View mLaunchingViewCuj;
 
     private FloatingTaskView mFirstFloatingTaskView;
     private SplitInstructionsView mSplitInstructionsView;
@@ -187,12 +181,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;
@@ -276,17 +269,15 @@
                     // Loop through tasks in reverse, since they are ordered with recent tasks last
                     for (int j = taskGroups.size() - 1; j >= 0; j--) {
                         GroupTask groupTask = taskGroups.get(j);
-                        Task task1 = groupTask.task1;
-                        // Don't add duplicate Tasks
-                        if (isInstanceOfComponent(task1, key)
-                                && !Arrays.asList(lastActiveTasks).contains(task1)) {
-                            lastActiveTask = task1;
-                            break;
+                        // Account for desktop cases where there can be N tasks in the group
+                        for (Task task : groupTask.getTasks()) {
+                            if (isInstanceOfComponent(task, key)
+                                    && !Arrays.asList(lastActiveTasks).contains(task)) {
+                                lastActiveTask = task;
+                                break;
+                            }
                         }
-                        Task task2 = groupTask.task2;
-                        if (isInstanceOfComponent(task2, key)
-                                && !Arrays.asList(lastActiveTasks).contains(task2)) {
-                            lastActiveTask = task2;
+                        if (lastActiveTask != null) {
                             break;
                         }
                     }
@@ -373,7 +364,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);
     }
 
     /**
@@ -381,7 +372,7 @@
      * ratio and no callback.
      */
     public void launchSplitTasks() {
-        launchSplitTasks(SNAP_TO_50_50, null);
+        launchSplitTasks(SNAP_TO_2_50_50, null);
     }
 
     /**
@@ -460,77 +451,41 @@
         Bundle optionsBundle = options1.toBundle();
         Bundle extrasBundle = new Bundle(1);
         extrasBundle.putParcelable(KEY_EXTRA_WIDGET_INTENT, widgetIntent);
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback, "LaunchSplitPair");
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_TASK_TASK ->
-                        mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
-                                null /* options2 */, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+        final RemoteTransition remoteTransition = getRemoteTransition(firstTaskId,
+                secondTaskId, callback, "LaunchSplitPair");
+        switch (launchData.getSplitLaunchType()) {
+            case SPLIT_TASK_TASK ->
+                    mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
+                            null /* options2 */, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_TASK_PENDINGINTENT ->
-                        mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle,
-                                firstTaskId, extrasBundle, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+            case SPLIT_TASK_PENDINGINTENT ->
+                    mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle,
+                            firstTaskId, extrasBundle, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_TASK_SHORTCUT ->
-                        mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle,
-                                firstTaskId, null /*options2*/, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+            case SPLIT_TASK_SHORTCUT ->
+                    mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle,
+                            firstTaskId, null /*options2*/, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_PENDINGINTENT_TASK ->
-                        mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle,
-                                secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+            case SPLIT_PENDINGINTENT_TASK ->
+                    mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle,
+                            secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_PENDINGINTENT_PENDINGINTENT ->
-                        mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut,
-                                optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle,
-                                initialStagePosition, snapPosition, remoteTransition,
-                                shellInstanceId);
+            case SPLIT_PENDINGINTENT_PENDINGINTENT ->
+                    mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut,
+                            optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle,
+                            initialStagePosition, snapPosition, remoteTransition,
+                            shellInstanceId);
 
-                case SPLIT_SHORTCUT_TASK ->
-                        mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle,
-                                secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
-            }
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, secondTaskId,
-                    callback);
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_TASK_TASK ->
-                        mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle,
-                                secondTaskId, null /* options2 */, initialStagePosition,
-                                snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_TASK_PENDINGINTENT ->
-                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(secondPI,
-                                secondUserId, optionsBundle, firstTaskId, null /*options2*/,
-                                initialStagePosition, snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_TASK_SHORTCUT ->
-                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(secondShortcut,
-                                optionsBundle, firstTaskId, null /*options2*/, initialStagePosition,
-                                snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_PENDINGINTENT_TASK ->
-                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId,
-                                optionsBundle, secondTaskId, null /*options2*/,
-                                initialStagePosition, snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_PENDINGINTENT_PENDINGINTENT ->
-                        mSystemUiProxy.startIntentsWithLegacyTransition(firstPI, firstUserId,
-                                firstShortcut, optionsBundle, secondPI, secondUserId,
-                                secondShortcut, null /*options2*/, initialStagePosition,
-                                snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_SHORTCUT_TASK ->
-                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(firstShortcut,
-                                optionsBundle, secondTaskId, null /*options2*/,
-                                initialStagePosition, snapPosition, adapter, shellInstanceId);
-            }
+            case SPLIT_SHORTCUT_TASK ->
+                    mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle,
+                            secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
         }
+
     }
 
     /**
@@ -576,20 +531,13 @@
         }
         Bundle optionsBundle = options1.toBundle();
 
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            final RemoteTransition transition = remoteTransition == null
-                    ? getShellRemoteTransition(
-                            firstTaskId, secondTaskId, callback, "LaunchExistingPair")
-                    : remoteTransition;
-            mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
-                    stagePosition, snapPosition, transition, null /*shellInstanceId*/);
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId,
-                    secondTaskId, callback);
-            mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, secondTaskId,
-                    null /* options2 */, stagePosition, snapPosition, adapter,
-                    null /*shellInstanceId*/);
-        }
+        final RemoteTransition transition = remoteTransition == null
+                ? getRemoteTransition(
+                firstTaskId, secondTaskId, callback, "LaunchExistingPair")
+                : remoteTransition;
+        mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
+                stagePosition, snapPosition, transition, null /*shellInstanceId*/);
+
     }
 
     /**
@@ -615,34 +563,16 @@
                 ActivityThread.currentActivityThread().getApplicationThread(),
                 "LaunchAppFullscreen");
         InstanceId instanceId = mSessionInstanceIds.first;
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
-                        optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
-                        SNAP_TO_50_50, remoteTransition, instanceId);
-                case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
-                        firstUserId, optionsBundle, secondTaskId, null /*options2*/,
-                        initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
-                case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
-                        initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
-                        initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
-            }
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId,
-                    secondTaskId, callback);
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasksWithLegacyTransition(
-                        firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
-                        initialStagePosition, SNAP_TO_50_50, adapter, instanceId);
-                case SPLIT_SINGLE_INTENT_FULLSCREEN ->
-                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId,
-                                optionsBundle, secondTaskId, null /*options2*/,
-                                initialStagePosition, SNAP_TO_50_50, adapter, instanceId);
-                case SPLIT_SINGLE_SHORTCUT_FULLSCREEN ->
-                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(
-                                initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
-                                initialStagePosition, SNAP_TO_50_50, adapter, instanceId);
-            }
+        switch (launchData.getSplitLaunchType()) {
+            case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
+                    optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
+                    SNAP_TO_2_50_50, remoteTransition, instanceId);
+            case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
+                    firstUserId, optionsBundle, secondTaskId, null /*options2*/,
+                    initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
+            case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
+                    initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
+                    initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
         }
     }
 
@@ -660,7 +590,7 @@
         mSplitFromDesktopController = controller;
     }
 
-    private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
+    private RemoteTransition getRemoteTransition(int firstTaskId, int secondTaskId,
             @Nullable Consumer<Boolean> callback, String transitionName) {
         final RemoteSplitLaunchTransitionRunner animationRunner =
                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
@@ -668,14 +598,6 @@
                 ActivityThread.currentActivityThread().getApplicationThread(), transitionName);
     }
 
-    private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
-            @Nullable Consumer<Boolean> callback) {
-        final RemoteSplitLaunchAnimationRunner animationRunner =
-                new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback);
-        return new RemoteAnimationAdapter(animationRunner, 300, 150,
-                ActivityThread.currentActivityThread().getApplicationThread());
-    }
-
     /**
      * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from
      * {@link #mSplitSelectDataHolder}
@@ -728,7 +650,12 @@
         return mSplitAnimationController;
     }
 
-    public void setLaunchingCuj(int launchCuj) {
+    /**
+     * Set params to invoke a trace session for the given view and CUJ when we begin animating the
+     * split launch AFTER we get a response from Shell.
+     */
+    public void setLaunchingCuj(View launchingView, int launchCuj) {
+        mLaunchingViewCuj = launchingView;
         mLaunchCuj = launchCuj;
     }
 
@@ -766,6 +693,9 @@
                         && mLaunchingTaskView.getRecentsView() != null
                         && mLaunchingTaskView.getRecentsView().isTaskViewVisible(
                         mLaunchingTaskView);
+                if (mLaunchingViewCuj != null && mLaunchCuj != -1) {
+                    InteractionJankMonitorWrapper.begin(mLaunchingViewCuj, mLaunchCuj);
+                }
                 mSplitAnimationController.playSplitLaunchAnimation(
                         shouldLaunchFromTaskView ? mLaunchingTaskView : null,
                         mLaunchingIconView,
@@ -807,55 +737,6 @@
     }
 
     /**
-     * LEGACY
-     * Remote animation runner for animation to launch an app.
-     */
-    private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat {
-
-        private final int mInitialTaskId;
-        private final int mSecondTaskId;
-        private final Consumer<Boolean> mSuccessCallback;
-
-        RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
-                @Nullable Consumer<Boolean> successCallback) {
-            mInitialTaskId = initialTaskId;
-            mSecondTaskId = secondTaskId;
-            mSuccessCallback = successCallback;
-        }
-
-        @Override
-        public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
-                RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
-                Runnable finishedCallback) {
-            postAsyncCallback(mHandler,
-                    () -> mSplitAnimationController
-                            .playSplitLaunchAnimation(mLaunchingTaskView,
-                            mLaunchingIconView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
-                            nonApps, mStateManager, mDepthController, null /* info */, null /* t */,
-                            () -> {
-                                finishedCallback.run();
-                                if (mSuccessCallback != null) {
-                                    mSuccessCallback.accept(true);
-                                }
-                                resetState();
-                            },
-                            QuickStepContract.getWindowCornerRadius(mContainer.asContext())));
-        }
-
-        @Override
-        public void onAnimationCancelled() {
-            postAsyncCallback(mHandler, () -> {
-                if (mSuccessCallback != null) {
-                    // Launching legacy tasks while recents animation is running will always cause
-                    // onAnimationCancelled to be called (should be fixed w/ shell transitions?)
-                    mSuccessCallback.accept(mRecentsAnimationRunning);
-                }
-                resetState();
-            });
-        }
-    }
-
-    /**
      * To be called whenever we exit split selection state. If
      * {@link FeatureFlags#enableSplitContextually()} is set, this should be the
      * central way split is getting reset, which should then go through the callbacks to reset
@@ -863,6 +744,7 @@
      */
     public void resetState() {
         mSplitSelectDataHolder.resetState();
+        mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
         dispatchOnSplitSelectionExit();
         mRecentsAnimationRunning = false;
         mLaunchingTaskView = null;
@@ -877,6 +759,7 @@
             InteractionJankMonitorWrapper.end(mLaunchCuj);
         }
         mLaunchCuj = -1;
+        mLaunchingViewCuj = null;
 
         if (mSessionInstanceIds != null) {
             mStatsLogManager.logger()
@@ -961,7 +844,7 @@
         private final int mSplitPlaceholderSize;
         private final int mSplitPlaceholderInset;
         private ActivityManager.RunningTaskInfo mTaskInfo;
-        private ISplitSelectListener mSplitSelectListener;
+        private DesktopSplitSelectListenerImpl mSplitSelectListener;
         private Drawable mAppIcon;
 
         public SplitFromDesktopController(QuickstepLauncher launcher,
@@ -972,21 +855,14 @@
                     R.dimen.split_placeholder_size);
             mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
                     R.dimen.split_placeholder_inset);
-            mSplitSelectListener = new ISplitSelectListener.Stub() {
-                @Override
-                public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
-                        int splitPosition, Rect taskBounds) {
-                    MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition,
-                            taskBounds));
-                    return true;
-                }
-            };
+            mSplitSelectListener = new DesktopSplitSelectListenerImpl(this);
             SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener);
         }
 
         void onDestroy() {
             SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener(
                     mSplitSelectListener);
+            mSplitSelectListener.release();
             mSplitSelectListener = null;
         }
 
@@ -1009,20 +885,22 @@
                 Log.w(TAG, "Package not found: " + packageName, e);
             }
             RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
-                    SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()),
-                    false /* allowMinimizeSplitScreen */);
+                    SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()));
 
             DesktopSplitRecentsAnimationListener listener =
                     new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds);
 
-            MAIN_EXECUTOR.execute(() -> {
-                callbacks.addListener(listener);
-                UI_HELPER_EXECUTOR.execute(
-                        // Transition from app to enter stage split in launcher with
-                        // recents animation.
-                        () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
-                                mOverviewComponentObserver.getOverviewIntent(),
-                                SystemClock.uptimeMillis(), callbacks, null, null));
+            callbacks.addListener(listener);
+            UI_HELPER_EXECUTOR.execute(() -> {
+                // Transition from app to enter stage split in launcher with recents animation
+                final ActivityOptions options = ActivityOptions.makeBasic();
+                options.setPendingIntentBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+                options.setTransientLaunch();
+                SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
+                        .startRecentsActivity(
+                                mOverviewComponentObserver.getOverviewIntent(), options,
+                                callbacks, false /* useSyntheticRecentsTransition */);
             });
         }
 
@@ -1066,7 +944,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
@@ -1074,9 +961,49 @@
                         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();
             }
         }
     }
+
+    /**
+     * Wrapper for the ISplitSelectListener stub to prevent lingering references to the launcher
+     * activity via the controller.
+     */
+    private static class DesktopSplitSelectListenerImpl extends ISplitSelectListener.Stub {
+
+        private SplitFromDesktopController mController;
+
+        DesktopSplitSelectListenerImpl(@NonNull SplitFromDesktopController controller) {
+            mController = controller;
+        }
+
+        /**
+         * Clears any references to the controller.
+         */
+        void release() {
+            mController = null;
+        }
+
+        @Override
+        public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+                int splitPosition, Rect taskBounds) {
+            MAIN_EXECUTOR.execute(() -> {
+                if (mController != null) {
+                    mController.enterSplitSelect(taskInfo, splitPosition, taskBounds);
+                }
+            });
+            return true;
+        }
+    }
 }
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 27fb31d..744c08c 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -27,9 +27,9 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.os.SystemClock;
 
 import androidx.annotation.BinderThread;
 
@@ -84,19 +84,24 @@
             return;
         }
         RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
-                SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()),
-                false /* allowMinimizeSplitScreen */);
+                SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()));
         SplitWithKeyboardShortcutRecentsAnimationListener listener =
                 new SplitWithKeyboardShortcutRecentsAnimationListener(leftOrTop);
 
         MAIN_EXECUTOR.execute(() -> {
             callbacks.addListener(listener);
-            UI_HELPER_EXECUTOR.execute(
-                    // Transition from fullscreen app to enter stage split in launcher with
-                    // recents animation.
-                    () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
-                            mOverviewComponentObserver.getOverviewIntent(),
-                            SystemClock.uptimeMillis(), callbacks, null, null));
+            UI_HELPER_EXECUTOR.execute(() -> {
+                // Transition from fullscreen app to enter stage split in launcher with
+                // recents animation
+                final ActivityOptions options = ActivityOptions.makeBasic();
+                options.setPendingIntentBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+                options.setTransientLaunch();
+                SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
+                        .startRecentsActivity(mOverviewComponentObserver.getOverviewIntent(),
+                                ActivityOptions.makeBasic(), callbacks,
+                                false /* useSyntheticRecentsTransition */);
+            });
         });
     }
 
@@ -110,17 +115,17 @@
 
         private final boolean mLeftOrTop;
         private final Rect mTempRect = new Rect();
+        private final ActivityManager.RunningTaskInfo mRunningTaskInfo;
 
         private SplitWithKeyboardShortcutRecentsAnimationListener(boolean leftOrTop) {
             mLeftOrTop = leftOrTop;
+            mRunningTaskInfo = ActivityManagerWrapper.getInstance().getRunningTask();
         }
 
         @Override
         public void onRecentsAnimationStart(RecentsAnimationController controller,
                 RecentsAnimationTargets targets) {
-            ActivityManager.RunningTaskInfo runningTaskInfo =
-                    ActivityManagerWrapper.getInstance().getRunningTask();
-            mController.setInitialTaskSelect(runningTaskInfo,
+            mController.setInitialTaskSelect(mRunningTaskInfo,
                     mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT,
                     null /* itemInfo */,
                     mLeftOrTop ? LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP
@@ -136,9 +141,9 @@
             RectF startingTaskRect = new RectF();
             final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
                     mLauncher, mLauncher.getDragLayer(),
-                    controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(),
+                    controller.screenshotTask(mRunningTaskInfo.taskId).getThumbnail(),
                     null /* icon */, startingTaskRect);
-            Task task = Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
+            Task task = Task.from(new Task.TaskKey(mRunningTaskInfo), mRunningTaskInfo,
                     false /* isLocked */);
             RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
                     .getIconCache()
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 56e91ed..828322b 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -39,7 +39,7 @@
 import com.android.quickstep.TaskAnimationManager;
 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-import com.android.wm.shell.pip.PipContentOverlay;
+import com.android.wm.shell.shared.pip.PipContentOverlay;
 
 /**
  * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window
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/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 304b8f4..f3b984b8 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.util;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.content.Context;
@@ -30,7 +31,8 @@
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
-import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.SystemUiProxy;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.util.List;
 import java.util.Set;
@@ -40,8 +42,17 @@
  */
 public class SystemWindowManagerProxy extends WindowManagerProxy {
 
+    private final TISBindHelper mTISBindHelper;
+
     public SystemWindowManagerProxy(Context context) {
         super(true);
+        mTISBindHelper = new TISBindHelper(context, binder -> {});
+    }
+
+    @Override
+    public void close() {
+        super.close();
+        mTISBindHelper.onDestroy();
     }
 
     @Override
@@ -53,11 +64,29 @@
     @Override
     public boolean isInDesktopMode() {
         DesktopVisibilityController desktopController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+                mTISBindHelper.getDesktopVisibilityController();
         return desktopController != null && desktopController.areDesktopTasksVisible();
     }
 
     @Override
+    public boolean showLockedTaskbarOnHome(Context displayInfoContext) {
+        if (!DesktopModeStatus.canEnterDesktopMode(displayInfoContext)) {
+            return false;
+        }
+        if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(displayInfoContext)) {
+            return false;
+        }
+        final boolean isFreeformDisplay = displayInfoContext.getResources().getConfiguration()
+                .windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+        return isFreeformDisplay;
+    }
+
+    @Override
+    public boolean isHomeVisible(Context context) {
+        return SystemUiProxy.INSTANCE.get(context).getHomeVisibilityState().isHomeVisible();
+    }
+
+    @Override
     public int getRotation(Context displayInfoContext) {
         return displayInfoContext.getResources().getConfiguration().windowConfiguration
                 .getRotation();
diff --git a/quickstep/src/com/android/quickstep/util/TISBindHelper.java b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
index 9a01042..b573604 100644
--- a/quickstep/src/com/android/quickstep/util/TISBindHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
@@ -25,6 +25,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.TaskbarManager;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.TouchInteractionService;
@@ -108,6 +109,11 @@
         return mBinder == null ? null : mBinder.getTaskbarManager();
     }
 
+    @Nullable
+    public DesktopVisibilityController getDesktopVisibilityController() {
+        return mBinder == null ? null : mBinder.getDesktopVisibilityController();
+    }
+
     /**
      * Sets flag whether a predictive back-to-home animation is in progress
      */
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
index 98d363e..498078b 100644
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
@@ -22,13 +22,13 @@
 import com.android.launcher3.util.IntArray;
 
 import java.lang.annotation.Retention;
+import java.util.List;
 
 /**
  * Helper class for navigating RecentsView grid tasks via arrow keys and tab.
  */
 public class TaskGridNavHelper {
     public static final int CLEAR_ALL_PLACEHOLDER_ID = -1;
-    public static final int INVALID_FOCUSED_TASK_ID = -1;
 
     public static final int DIRECTION_UP = 0;
     public static final int DIRECTION_DOWN = 1;
@@ -43,25 +43,25 @@
     private final IntArray mOriginalTopRowIds;
     private IntArray mTopRowIds;
     private IntArray mBottomRowIds;
-    private final int mFocusedTaskId;
 
-    public TaskGridNavHelper(IntArray topIds, IntArray bottomIds, int focusedTaskId) {
-        mFocusedTaskId = focusedTaskId;
+    public TaskGridNavHelper(IntArray topIds, IntArray bottomIds,
+            List<Integer> largeTileIds) {
         mOriginalTopRowIds = topIds.clone();
-        generateTaskViewIdGrid(topIds, bottomIds);
+        generateTaskViewIdGrid(topIds, bottomIds, largeTileIds);
     }
 
-    private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray) {
-        boolean hasFocusedTask = mFocusedTaskId != INVALID_FOCUSED_TASK_ID;
-        int maxSize =
-                Math.max(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0);
-        int minSize =
-                Math.min(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0);
+    private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray,
+            List<Integer> largeTileIds) {
 
-        // Add the focused task to the beginning of both arrays if it exists.
-        if (hasFocusedTask) {
-            topRowIdArray.add(0, mFocusedTaskId);
-            bottomRowIdArray.add(0, mFocusedTaskId);
+        int maxSize = Math.max(topRowIdArray.size(), bottomRowIdArray.size())
+                + largeTileIds.size();
+        int minSize = Math.min(topRowIdArray.size(), bottomRowIdArray.size())
+                + largeTileIds.size();
+
+        // Add Large tile task views first at the beginning
+        for (int i = 0; i < largeTileIds.size(); i++) {
+            topRowIdArray.add(i, largeTileIds.get(i));
+            bottomRowIdArray.add(i, largeTileIds.get(i));
         }
 
         // Fill in the shorter array with the ids from the longer one.
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 d9b7d20..a4b8fec 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -24,7 +24,6 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
-import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
 import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
 
@@ -121,6 +120,7 @@
     private boolean mScaleToCarouselTaskSize = false;
     private int mTaskRectTranslationX;
     private int mTaskRectTranslationY;
+    private int mDesktopTaskIndex = 0;
 
     public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) {
         mContext = context;
@@ -171,7 +171,6 @@
             mTaskRect.set(mFullTaskSize);
             mOrientationState.getOrientationHandler()
                     .setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mStagePosition);
-            mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
         } else if (mIsDesktopTask) {
             // For desktop, tasks can take up only part of the screen size.
             // Full task size represents the whole screen size, but scaled down to fit in recents.
@@ -185,10 +184,19 @@
             mTaskRect.scale(scale);
             // Ensure the task rect is inside the full task rect
             mTaskRect.offset(mFullTaskSize.left, mFullTaskSize.top);
+
+            Rect taskDimension = new Rect(0, 0, (int) fullscreenTaskDimension.x,
+                    (int) fullscreenTaskDimension.y);
+            mTmpCropRect.set(mThumbnailPosition);
+            if (mTmpCropRect.setIntersect(taskDimension, mThumbnailPosition)) {
+                mTmpCropRect.offset(-mThumbnailPosition.left, -mThumbnailPosition.top);
+            } else {
+                mTmpCropRect.setEmpty();
+            }
         } else {
             mTaskRect.set(mFullTaskSize);
-            mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
         }
+        mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
     }
 
     /**
@@ -283,8 +291,9 @@
     /**
      * Sets whether this task is part of desktop tasks in overview.
      */
-    public void setIsDesktopTask(boolean desktop) {
+    public void setIsDesktopTask(boolean desktop, int index) {
         mIsDesktopTask = desktop;
+        mDesktopTaskIndex = index;
     }
 
     /**
@@ -298,6 +307,14 @@
     }
 
     /**
+     * Override the pivot used to apply scale changes.
+     */
+    public void setPivotOverride(PointF pivotOverride) {
+        mPivotOverride = pivotOverride;
+        getFullScreenScale();
+    }
+
+    /**
      * Adds animation for all the components corresponding to transition from an app to overview.
      */
     public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) {
@@ -486,10 +503,12 @@
                 recentsViewPrimaryTranslation.value);
         applyWindowToHomeRotation(mMatrix);
 
-        // Crop rect is the inverse of thumbnail matrix
-        mTempRectF.set(0, 0, taskWidth, taskHeight);
-        mInversePositionMatrix.mapRect(mTempRectF);
-        mTempRectF.roundOut(mTmpCropRect);
+        if (!mIsDesktopTask) {
+            // Crop rect is the inverse of thumbnail matrix
+            mTempRectF.set(0, 0, taskWidth, taskHeight);
+            mInversePositionMatrix.mapRect(mTempRectF);
+            mTempRectF.roundOut(mTmpCropRect);
+        }
 
         params.setProgress(1f - fullScreenProgress);
         params.applySurfaceParams(surfaceTransaction == null
@@ -525,21 +544,12 @@
 
         // If mDrawsBelowRecents is unset, no reordering will be enforced.
         if (mDrawsBelowRecents != null) {
-            // In legacy transitions, the animation leashes remain in same hierarchy in the
-            // TaskDisplayArea, so we don't want to bump the layer too high otherwise it will
-            // conflict with layers that WM core positions (ie. the input consumers).  For shell
-            // transitions, the animation leashes are reparented to an animation container so we
-            // can bump layers as needed.
-            if (ENABLE_SHELL_TRANSITIONS) {
-                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);
-            } else {
-                builder.setLayer(mDrawsBelowRecents
-                        ? Integer.MIN_VALUE + app.prefixOrderIndex
-                        : 0);
-            }
+            // In shell transitions, the animation leashes are reparented to an animation container
+            // so we can bump layers as needed.
+            builder.setLayer(mDrawsBelowRecents
+                    // 1000 is an arbitrary number to give room for multiple layers.
+                    ? Integer.MIN_VALUE + 1000 + app.prefixOrderIndex - mDesktopTaskIndex
+                    : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex - mDesktopTaskIndex);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
index 66bff73..519ef60 100644
--- a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.util;
 
+import android.annotation.NonNull;
 import android.os.UserHandle;
 
 import com.android.systemui.shared.recents.model.Task;
@@ -36,7 +37,7 @@
     /**
      * Called when the icon for a task changes
      */
-    default void onTaskIconChanged(String pkg, UserHandle user) {}
+    default void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {}
 
     /**
      * Called when the icon for a task changes
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/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
index 09563f5..915c9e5 100644
--- a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
+++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
@@ -22,7 +22,6 @@
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
 import com.android.launcher3.anim.PendingAnimation
-import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.uioverrides.QuickstepLauncher
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
@@ -30,7 +29,7 @@
 /** Controls animations that are happening during unfolding foldable devices */
 class LauncherUnfoldTransitionController(
     private val launcher: QuickstepLauncher,
-    private val progressProvider: ProxyUnfoldTransitionProvider
+    private val progressProvider: ProxyUnfoldTransitionProvider,
 ) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener {
 
     private var isTablet: Boolean? = null
@@ -57,10 +56,6 @@
     }
 
     override fun onDeviceProfileChanged(dp: DeviceProfile) {
-        if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
-            return
-        }
-
         if (isTablet != null && dp.isTablet != isTablet) {
             // We should preemptively start the animation only if:
             // - We changed to the unfolded screen
@@ -93,7 +88,7 @@
             provider = this,
             factory = this::onPrepareUnfoldAnimation,
             duration =
-                1000L // The expected duration for the animation. Then only comes to play if we have
+                1000L, // The expected duration for the animation. Then only comes to play if we have
             // to run the animation ourselves in case sysui misses the end signal
         )
         timeoutAlarm.cancelAlarm()
@@ -119,7 +114,7 @@
             launcher,
             isVertical,
             dp.displayInfo.currentSize,
-            anim
+            anim,
         )
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
new file mode 100644
index 0000000..481acac
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import com.android.quickstep.views.TaskView.FullscreenDrawParams
+
+class DesktopTaskContentView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
+    private val currentFullscreenParams = FullscreenDrawParams(context)
+    private val taskCornerRadius: Float
+        get() = currentFullscreenParams.cornerRadius
+
+    private val bounds = Rect()
+
+    init {
+        clipToOutline = true
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    outline.setRoundRect(bounds, taskCornerRadius)
+                }
+            }
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        bounds.set(0, 0, w, h)
+        invalidateOutline()
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 9ce2277..5e842aa 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -15,21 +15,24 @@
  */
 package com.android.quickstep.views
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
-import android.graphics.drawable.LayerDrawable
 import android.graphics.drawable.ShapeDrawable
 import android.graphics.drawable.shapes.RoundRectShape
 import android.util.AttributeSet
 import android.util.Log
-import android.view.LayoutInflater
+import android.view.Gravity
 import android.view.View
-import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.core.content.res.ResourcesCompat
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
+import com.android.launcher3.testing.TestLogging
+import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TransformingTouchDelegate
@@ -37,6 +40,8 @@
 import com.android.launcher3.util.rects.set
 import com.android.quickstep.BaseContainerInterface
 import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.ViewUtils
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.systemui.shared.recents.model.Task
 
@@ -50,24 +55,39 @@
             override fun computeTaskCornerRadius(context: Context) =
                 computeWindowCornerRadius(context)
         }
+
     private val taskThumbnailViewDeprecatedPool =
-        ViewPool<TaskThumbnailViewDeprecated>(
-            context,
-            this,
-            R.layout.task_thumbnail_deprecated,
-            VIEW_POOL_MAX_SIZE,
-            VIEW_POOL_INITIAL_SIZE
-        )
+        if (!enableRefactorTaskThumbnail()) {
+            ViewPool<TaskThumbnailViewDeprecated>(
+                context,
+                this,
+                R.layout.task_thumbnail_deprecated,
+                VIEW_POOL_MAX_SIZE,
+                VIEW_POOL_INITIAL_SIZE,
+            )
+        } else null
+
+    private val taskThumbnailViewPool =
+        if (enableRefactorTaskThumbnail()) {
+            ViewPool<TaskThumbnailView>(
+                context,
+                this,
+                R.layout.task_thumbnail,
+                VIEW_POOL_MAX_SIZE,
+                VIEW_POOL_INITIAL_SIZE,
+            )
+        } else null
+
     private val tempPointF = PointF()
     private val tempRect = Rect()
     private lateinit var backgroundView: View
     private lateinit var iconView: TaskViewIcon
-    private var childCountAtInflation = 0
+    private lateinit var contentView: FrameLayout
 
     override fun onFinishInflate() {
         super.onFinishInflate()
         backgroundView =
-            findViewById<View>(R.id.background)!!.apply {
+            findViewById<View>(R.id.background).apply {
                 updateLayoutParams<LayoutParams> {
                     topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
                 }
@@ -77,25 +97,36 @@
                             setTint(
                                 resources.getColor(
                                     android.R.color.system_neutral2_300,
-                                    context.theme
+                                    context.theme,
                                 )
                             )
                         }
             }
         iconView =
             getOrInflateIconView(R.id.icon).apply {
-                val iconBackground = resources.getDrawable(R.drawable.bg_circle, context.theme)
-                val icon = resources.getDrawable(R.drawable.ic_desktop, context.theme)
-                setIcon(this, LayerDrawable(arrayOf(iconBackground, icon)))
+                setIcon(
+                    this,
+                    ResourcesCompat.getDrawable(
+                        context.resources,
+                        R.drawable.ic_desktop_with_bg,
+                        context.theme,
+                    ),
+                )
+                setText(resources.getText(R.string.recent_task_desktop))
             }
-        childCountAtInflation = childCount
+        contentView =
+            findViewById<FrameLayout>(R.id.desktop_content).apply {
+                updateLayoutParams<LayoutParams> {
+                    topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
+                }
+            }
     }
 
     /** Updates this desktop task to the gives task list defined in `tasks` */
     fun bind(
         tasks: List<Task>,
         orientedState: RecentsOrientedState,
-        taskOverlayFactory: TaskOverlayFactory
+        taskOverlayFactory: TaskOverlayFactory,
     ) {
         if (DEBUG) {
             val sb = StringBuilder()
@@ -108,21 +139,12 @@
             tasks.map { task ->
                 val snapshotView =
                     if (enableRefactorTaskThumbnail()) {
-                        LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false)
+                        taskThumbnailViewPool!!.view
                     } else {
-                        taskThumbnailViewDeprecatedPool.view
+                        taskThumbnailViewDeprecatedPool!!.view
                     }
+                contentView.addView(snapshotView, 0)
 
-                addView(
-                    snapshotView,
-                    // Add snapshotView to the front after initial views e.g. icon and
-                    // background.
-                    childCountAtInflation,
-                    LayoutParams(
-                        ViewGroup.LayoutParams.WRAP_CONTENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT
-                    )
-                )
                 TaskContainer(
                     this,
                     task,
@@ -132,7 +154,7 @@
                     SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
                     digitalWellBeingToast = null,
                     showWindowsView = null,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 )
             }
         taskContainers.forEach { it.bind() }
@@ -143,36 +165,47 @@
         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)
             }
         }
     }
 
-    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-        val containerWidth = MeasureSpec.getSize(widthMeasureSpec)
-        var containerHeight = MeasureSpec.getSize(heightMeasureSpec)
-        setMeasuredDimension(containerWidth, containerHeight)
-
+    @SuppressLint("RtlHardcoded")
+    override fun updateTaskSize(
+        lastComputedTaskSize: Rect,
+        lastComputedGridTaskSize: Rect,
+        lastComputedCarouselTaskSize: Rect,
+    ) {
+        super.updateTaskSize(
+            lastComputedTaskSize,
+            lastComputedGridTaskSize,
+            lastComputedCarouselTaskSize,
+        )
         if (taskContainers.isEmpty()) {
             return
         }
 
         val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
-        containerHeight -= thumbnailTopMarginPx
+
+        val containerWidth = layoutParams.width
+        val containerHeight = layoutParams.height - thumbnailTopMarginPx
 
         BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+
         val windowWidth = tempPointF.x.toInt()
         val windowHeight = tempPointF.y.toInt()
         val scaleWidth = containerWidth / windowWidth.toFloat()
         val scaleHeight = containerHeight / windowHeight.toFloat()
+
         if (DEBUG) {
             Log.d(
                 TAG,
-                "onMeasure: container=[$containerWidth,$containerHeight] " +
-                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]"
+                "onMeasure: container=[$containerWidth,$containerHeight]" +
+                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]",
             )
         }
 
@@ -187,33 +220,38 @@
                         right = windowWidth / 4
                         bottom = windowHeight / 4
                     }
-            val thumbWidth = (taskSize.width() * scaleWidth).toInt()
-            val thumbHeight = (taskSize.height() * scaleHeight).toInt()
-            it.snapshotView.measure(
-                MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY)
-            )
+            val positionInParent = it.task.positionInParent ?: ORIGIN
 
             // Position the task to the same position as it would be on the desktop
-            val positionInParent = it.task.positionInParent ?: ORIGIN
-            val taskX = (positionInParent.x * scaleWidth).toInt()
-            var taskY = (positionInParent.y * scaleHeight).toInt()
-            // move task down by margin size
-            taskY += thumbnailTopMarginPx
-            it.snapshotView.x = taskX.toFloat()
-            it.snapshotView.y = taskY.toFloat()
+            it.snapshotView.updateLayoutParams<LayoutParams> {
+                gravity = Gravity.LEFT or Gravity.TOP
+                width = (taskSize.width() * scaleWidth).toInt()
+                height = (taskSize.height() * scaleHeight).toInt()
+                leftMargin = (positionInParent.x * scaleWidth).toInt()
+                topMargin = (positionInParent.y * scaleHeight).toInt()
+            }
             if (DEBUG) {
-                Log.d(
-                    TAG,
-                    "onMeasure: task=${it.task.key} thumb=[$thumbWidth,$thumbHeight]" +
-                        " pos=[$taskX,$taskY]"
-                )
+                with(it.snapshotView.layoutParams as LayoutParams) {
+                    Log.d(
+                        TAG,
+                        "onMeasure: task=${it.task.key} size=[$width,$height]" +
+                            " margin=[$leftMargin,$topMargin]",
+                    )
+                }
             }
         }
     }
 
     override fun needsUpdate(dataChange: Int, flag: Int) =
-        if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false
+        if (flag == FLAG_UPDATE_CORNER_RADIUS) false else super.needsUpdate(dataChange, flag)
+
+    override fun onIconLoaded(taskContainer: TaskContainer) {
+        // Update contentDescription of snapshotView only, individual task icon is unused.
+        taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
+    }
+
+    // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
+    override fun onIconUnloaded(taskContainer: TaskContainer) {}
 
     // thumbnailView is laid out differently and is handled in onMeasure
     override fun updateThumbnailSize() {}
@@ -226,33 +264,46 @@
         }
     }
 
-    override fun launchTaskAnimated(): RunnableList? {
+    private fun launchTaskWithDesktopController(animated: Boolean): RunnableList? {
         val recentsView = recentsView ?: return null
+        TestLogging.recordEvent(
+            TestProtocol.SEQUENCE_MAIN,
+            "launchDesktopFromRecents",
+            taskIds.contentToString(),
+        )
         val endCallback = RunnableList()
         val desktopController = recentsView.desktopRecentsController
         checkNotNull(desktopController) { "recentsController is null" }
-        desktopController.launchDesktopFromRecents(this) { endCallback.executeAllAndDestroy() }
-        Log.d(TAG, "launchTaskAnimated - launchDesktopFromRecents: ${taskIds.contentToString()}")
+        desktopController.launchDesktopFromRecents(this, animated) {
+            endCallback.executeAllAndDestroy()
+        }
+        Log.d(
+            TAG,
+            "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated",
+        )
 
         // Callbacks get run from recentsView for case when recents animation already running
         recentsView.addSideTaskLaunchCallback(endCallback)
         return endCallback
     }
 
-    override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
-        launchTasks()
-        callback(true)
-    }
+    override fun launchAsStaticTile() = launchTaskWithDesktopController(animated = true)
 
-    // Desktop tile can't be in split screen
-    override fun confirmSecondSplitSelectApp(): Boolean = false
+    override fun launchWithoutAnimation(
+        isQuickSwitch: Boolean,
+        callback: (launched: Boolean) -> Unit,
+    ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(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) {}
 
     override fun onFullscreenProgressChanged(fullscreenProgress: Float) {
-        // Don't show background while we are transitioning to/from fullscreen
-        backgroundView.visibility = if (fullscreenProgress > 0) INVISIBLE else VISIBLE
+        backgroundView.alpha = 1 - fullscreenProgress
     }
 
     override fun updateCurrentFullscreenParams() {
@@ -262,10 +313,16 @@
 
     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
         private val ORIGIN = Point(0, 0)
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
deleted file mode 100644
index 9f268a0..0000000
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ /dev/null
@@ -1,444 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
-
-import static com.android.launcher3.Utilities.prefixTextWithIcon;
-import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR;
-
-import android.app.ActivityOptions;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.AppUsageLimit;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.icu.text.MeasureFormat;
-import android.icu.text.MeasureFormat.FormatWidth;
-import android.icu.util.Measure;
-import android.icu.util.MeasureUnit;
-import android.os.UserHandle;
-import android.util.Log;
-import android.util.Pair;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.time.Duration;
-import java.util.Locale;
-
-public final class DigitalWellBeingToast {
-
-    private static final float THRESHOLD_LEFT_ICON_ONLY = 0.4f;
-    private static final float THRESHOLD_RIGHT_ICON_ONLY = 0.6f;
-
-    /** Will span entire width of taskView with full text */
-    private static final int SPLIT_BANNER_FULLSCREEN = 0;
-    /** Used for grid task view, only showing icon and time */
-    private static final int SPLIT_GRID_BANNER_LARGE = 1;
-    /** Used for grid task view, only showing icon */
-    private static final int SPLIT_GRID_BANNER_SMALL = 2;
-
-    @IntDef(value = {
-            SPLIT_BANNER_FULLSCREEN,
-            SPLIT_GRID_BANNER_LARGE,
-            SPLIT_GRID_BANNER_SMALL,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    @interface SplitBannerConfig {
-    }
-
-    static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
-    static final int MINUTE_MS = 60000;
-
-    private static final String TAG = "DigitalWellBeingToast";
-
-    private final RecentsViewContainer mContainer;
-    private final TaskView mTaskView;
-    private final LauncherApps mLauncherApps;
-
-    private final int mBannerHeight;
-
-    private Task mTask;
-    private boolean mHasLimit;
-
-    private long mAppRemainingTimeMs;
-    @Nullable
-    private View mBanner;
-    private ViewOutlineProvider mOldBannerOutlineProvider;
-    private float mBannerOffsetPercentage;
-    @Nullable
-    private SplitBounds mSplitBounds;
-    private float mSplitOffsetTranslationY;
-    private float mSplitOffsetTranslationX;
-
-    private boolean mIsDestroyed = false;
-
-    public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) {
-        mContainer = container;
-        mTaskView = taskView;
-        mLauncherApps = container.asContext().getSystemService(LauncherApps.class);
-        mBannerHeight = container.asContext().getResources().getDimensionPixelSize(
-                R.dimen.digital_wellbeing_toast_height);
-    }
-
-    private void setNoLimit() {
-        mHasLimit = false;
-        mTaskView.setContentDescription(mTask.titleDescription);
-        replaceBanner(null);
-        mAppRemainingTimeMs = -1;
-    }
-
-    private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
-        mAppRemainingTimeMs = appRemainingTimeMs;
-        mHasLimit = true;
-        TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast,
-                mContainer.asContext(), mTaskView);
-        toast.setText(prefixTextWithIcon(mContainer.asContext(), R.drawable.ic_hourglass_top,
-                getText()));
-        toast.setOnClickListener(this::openAppUsageSettings);
-        replaceBanner(toast);
-
-        mTaskView.setContentDescription(
-                getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
-    }
-
-    public String getText() {
-        return getText(mAppRemainingTimeMs, false /* forContentDesc */);
-    }
-
-    public boolean hasLimit() {
-        return mHasLimit;
-    }
-
-    public void initialize(Task task) {
-        if (mIsDestroyed) {
-            throw new IllegalStateException("Cannot re-initialize a destroyed toast");
-        }
-        mTask = task;
-        ORDERED_BG_EXECUTOR.execute(() -> {
-            AppUsageLimit usageLimit = null;
-            try {
-                usageLimit = mLauncherApps.getAppUsageLimit(
-                        mTask.getTopComponent().getPackageName(),
-                        UserHandle.of(mTask.key.userId));
-            } catch (Exception e) {
-                Log.e(TAG, "Error initializing digital well being toast", e);
-            }
-            final long appUsageLimitTimeMs =
-                    usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
-            final long appRemainingTimeMs =
-                    usageLimit != null ? usageLimit.getUsageRemaining() : -1;
-
-            mTaskView.post(() -> {
-                if (mIsDestroyed) {
-                    return;
-                }
-                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
-                    setNoLimit();
-                } else {
-                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
-                }
-            });
-        });
-    }
-
-    /**
-     * Mark the DWB toast as destroyed and remove banner from TaskView.
-     */
-    public void destroy() {
-        mIsDestroyed = true;
-        mTaskView.post(() -> replaceBanner(null));
-    }
-
-    public void setSplitBounds(@Nullable SplitBounds splitBounds) {
-        mSplitBounds = splitBounds;
-    }
-
-    private @SplitBannerConfig int getSplitBannerConfig() {
-        if (mSplitBounds == null
-                || !mContainer.getDeviceProfile().isTablet
-                || mTaskView.isFocusedTask()) {
-            return SPLIT_BANNER_FULLSCREEN;
-        }
-
-        // For portrait grid only height of task changes, not width. So we keep the text the same
-        if (!mContainer.getDeviceProfile().isLeftRightSplit) {
-            return SPLIT_GRID_BANNER_LARGE;
-        }
-
-        // For landscape grid, for 30% width we only show icon, otherwise show icon and time
-        if (mTask.key.id == mSplitBounds.leftTopTaskId) {
-            return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY
-                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
-        } else {
-            return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY
-                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
-        }
-    }
-
-    private String getReadableDuration(
-            Duration duration,
-            @StringRes int durationLessThanOneMinuteStringId) {
-        int hours = Math.toIntExact(duration.toHours());
-        int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
-
-        // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
-        if (hours > 0 && minutes > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW)
-                    .formatMeasures(
-                            new Measure(hours, MeasureUnit.HOUR),
-                            new Measure(minutes, MeasureUnit.MINUTE));
-        }
-
-        // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
-        if (hours > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                    new Measure(hours, MeasureUnit.HOUR));
-        }
-
-        // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
-        if (minutes > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                    new Measure(minutes, MeasureUnit.MINUTE));
-        }
-
-        // Use a specific string for usage less than one minute but non-zero.
-        if (duration.compareTo(Duration.ZERO) > 0) {
-            return mContainer.asContext().getString(durationLessThanOneMinuteStringId);
-        }
-
-        // Otherwise, return 0-minute string.
-        return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                new Measure(0, MeasureUnit.MINUTE));
-    }
-
-    /**
-     * Returns text to show for the banner depending on {@link #getSplitBannerConfig()}
-     * If {@param forContentDesc} is {@code true}, this will always return the full
-     * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN}
-     */
-    private String getText(long remainingTime, boolean forContentDesc) {
-        final Duration duration = Duration.ofMillis(
-                remainingTime > MINUTE_MS ?
-                        (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
-                        remainingTime);
-        String readableDuration = getReadableDuration(duration,
-                R.string.shorter_duration_less_than_one_minute
-                /* forceFormatWidth */);
-        @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig();
-        if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
-            return mContainer.asContext().getString(
-                    R.string.time_left_for_app,
-                    readableDuration);
-        }
-
-        if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
-            // show no text
-            return "";
-        } else { // SPLIT_GRID_BANNER_LARGE
-            // only show time
-            return readableDuration;
-        }
-    }
-
-    public void openAppUsageSettings(View view) {
-        final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
-                .putExtra(Intent.EXTRA_PACKAGE_NAME,
-                        mTask.getTopComponent().getPackageName()).addFlags(
-                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        try {
-            final RecentsViewContainer container =
-                    RecentsViewContainer.containerFromContext(view.getContext());
-            final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
-                    view, 0, 0,
-                    view.getWidth(), view.getHeight());
-            container.asContext().startActivity(intent, options.toBundle());
-
-            // TODO: add WW logging on the app usage settings click.
-        } catch (ActivityNotFoundException e) {
-            Log.e(TAG, "Failed to open app usage settings for task "
-                    + mTask.getTopComponent().getPackageName(), e);
-        }
-    }
-
-    private String getContentDescriptionForTask(
-            Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
-        return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
-                mContainer.asContext().getString(
-                        R.string.task_contents_description_with_remaining_time,
-                        task.titleDescription,
-                        getText(appRemainingTimeMs, true /* forContentDesc */)) :
-                task.titleDescription;
-    }
-
-    private void replaceBanner(@Nullable View view) {
-        resetOldBanner();
-        setBanner(view);
-    }
-
-    private void resetOldBanner() {
-        if (mBanner != null) {
-            mBanner.setOutlineProvider(mOldBannerOutlineProvider);
-            mTaskView.removeView(mBanner);
-            mBanner.setOnClickListener(null);
-            mContainer.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner);
-        }
-    }
-
-    private void setBanner(@Nullable View view) {
-        mBanner = view;
-        if (mBanner != null && mTaskView.getRecentsView() != null) {
-            setupAndAddBanner();
-            setBannerOutline();
-        }
-    }
-
-    private void setupAndAddBanner() {
-        FrameLayout.LayoutParams layoutParams =
-                (FrameLayout.LayoutParams) mBanner.getLayoutParams();
-        DeviceProfile deviceProfile = mContainer.getDeviceProfile();
-        layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
-                mTaskView.getFirstSnapshotView().getLayoutParams()).bottomMargin;
-        RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
-        Pair<Float, Float> translations = orientationHandler
-                .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
-                        mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
-                        mTaskView.getSnapshotViews(), mTask.key.id, mBanner);
-        mSplitOffsetTranslationX = translations.first;
-        mSplitOffsetTranslationY = translations.second;
-        updateTranslationY();
-        updateTranslationX();
-        mTaskView.addView(mBanner);
-    }
-
-    private void setBannerOutline() {
-        // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
-        mOldBannerOutlineProvider = mBanner.getOutlineProvider() != null
-                ? mBanner.getOutlineProvider()
-                : ViewOutlineProvider.BACKGROUND;
-
-        mBanner.setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                mOldBannerOutlineProvider.getOutline(view, outline);
-                float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY;
-                outline.offset(0, Math.round(verticalTranslation));
-            }
-        });
-        mBanner.setClipToOutline(true);
-    }
-
-    void updateBannerOffset(float offsetPercentage) {
-        if (mBannerOffsetPercentage != offsetPercentage) {
-            mBannerOffsetPercentage = offsetPercentage;
-            if (mBanner != null) {
-                updateTranslationY();
-                mBanner.invalidateOutline();
-            }
-        }
-    }
-
-    private void updateTranslationY() {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setTranslationY(
-                (mBannerOffsetPercentage * mBannerHeight) + mSplitOffsetTranslationY);
-    }
-
-    private void updateTranslationX() {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setTranslationX(mSplitOffsetTranslationX);
-    }
-
-    void setBannerColorTint(int color, float amount) {
-        if (mBanner == null) {
-            return;
-        }
-        if (amount == 0) {
-            mBanner.setLayerType(View.LAYER_TYPE_NONE, null);
-        }
-        Paint layerPaint = new Paint();
-        layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
-        mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint);
-        mBanner.setLayerPaint(layerPaint);
-    }
-
-    void setBannerVisibility(int visibility) {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setVisibility(visibility);
-    }
-
-    private int getAccessibilityActionId() {
-        return (mSplitBounds != null
-                && mSplitBounds.rightBottomTaskId == mTask.key.id)
-                ? R.id.action_digital_wellbeing_bottom_right
-                : R.id.action_digital_wellbeing_top_left;
-    }
-
-    @Nullable
-    public AccessibilityNodeInfo.AccessibilityAction getDWBAccessibilityAction() {
-        if (!hasLimit()) {
-            return null;
-        }
-
-        Context context = mContainer.asContext();
-        String label =
-                (mTaskView.containsMultipleTasks())
-                        ? context.getString(
-                        R.string.split_app_usage_settings,
-                        TaskUtils.getTitle(context, mTask)
-                ) : context.getString(R.string.accessibility_app_usage_settings);
-        return new AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label);
-    }
-
-    public boolean handleAccessibilityAction(int action) {
-        if (getAccessibilityActionId() == action) {
-            openAppUsageSettings(mTaskView);
-            return true;
-        } else {
-            return false;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
new file mode 100644
index 0000000..c07b7fb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.AppUsageLimit
+import android.graphics.Outline
+import android.graphics.Paint
+import android.icu.text.MeasureFormat
+import android.icu.util.Measure
+import android.icu.util.MeasureUnit
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.AttributeSet
+import android.util.Log
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.component1
+import androidx.core.util.component2
+import androidx.core.view.isVisible
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
+import com.android.quickstep.TaskUtils
+import com.android.systemui.shared.recents.model.Task
+import java.time.Duration
+import java.util.Locale
+
+@SuppressLint("AppCompatCustomView")
+class DigitalWellBeingToast
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0,
+) : TextView(context, attrs, defStyleAttr, defStyleRes) {
+    private val recentsViewContainer: RecentsViewContainer =
+        RecentsViewContainer.containerFromContext(context)
+
+    private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java)
+
+    private val bannerHeight =
+        context.resources.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
+
+    private lateinit var task: Task
+    private lateinit var taskView: TaskView
+    private lateinit var snapshotView: View
+    @StagePosition private var stagePosition = STAGE_POSITION_UNDEFINED
+
+    private var appRemainingTimeMs: Long = 0
+    private var splitOffsetTranslationY = 0f
+        set(value) {
+            if (field != value) {
+                field = value
+                updateTranslationY()
+            }
+        }
+
+    private var isDestroyed = false
+
+    var hasLimit = false
+    var splitBounds: SplitConfigurationOptions.SplitBounds? = null
+    var bannerOffsetPercentage = 0f
+        set(value) {
+            if (field != value) {
+                field = value
+                updateTranslationY()
+            }
+        }
+
+    init {
+        setOnClickListener(::openAppUsageSettings)
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    BACKGROUND.getOutline(view, outline)
+                    val verticalTranslation = splitOffsetTranslationY - translationY
+                    outline.offset(0, Math.round(verticalTranslation))
+                }
+            }
+        clipToOutline = true
+    }
+
+    private fun setNoLimit() {
+        isVisible = false
+        hasLimit = false
+        appRemainingTimeMs = -1
+        setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
+    }
+
+    private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
+        isVisible = true
+        hasLimit = true
+        this.appRemainingTimeMs = appRemainingTimeMs
+        setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs)
+        text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
+    }
+
+    private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
+        val contentDescription =
+            getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
+        snapshotView.contentDescription = contentDescription
+    }
+
+    fun initialize() {
+        check(!isDestroyed) { "Cannot re-initialize a destroyed toast" }
+        setupTranslations()
+        Executors.ORDERED_BG_EXECUTOR.execute {
+            var usageLimit: AppUsageLimit? = null
+            try {
+                usageLimit =
+                    launcherApps?.getAppUsageLimit(
+                        task.topComponent.packageName,
+                        UserHandle.of(task.key.userId),
+                    )
+            } catch (e: Exception) {
+                Log.e(TAG, "Error initializing digital well being toast", e)
+            }
+            val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1
+            val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1
+
+            taskView.post {
+                if (isDestroyed) return@post
+                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+                    setNoLimit()
+                } else {
+                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs)
+                }
+            }
+        }
+    }
+
+    /** Bind the DWB toast to its dependencies. */
+    fun bind(
+        task: Task,
+        taskView: TaskView,
+        snapshotView: View,
+        @StagePosition stagePosition: Int,
+    ) {
+        this.task = task
+        this.taskView = taskView
+        this.snapshotView = snapshotView
+        this.stagePosition = stagePosition
+        isDestroyed = false
+    }
+
+    /** Mark the DWB toast as destroyed and hide it. */
+    fun destroy() {
+        isVisible = false
+        isDestroyed = true
+    }
+
+    private fun getSplitBannerConfig(): SplitBannerConfig {
+        val splitBounds = splitBounds
+        return when {
+            splitBounds == null ||
+                !recentsViewContainer.deviceProfile.isTablet ||
+                taskView.isLargeTile -> SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
+            // For portrait grid only height of task changes, not width. So we keep the text the
+            // same
+            !recentsViewContainer.deviceProfile.isLeftRightSplit ->
+                SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+            // For landscape grid, for 30% width we only show icon, otherwise show icon and time
+            task.key.id == splitBounds.leftTopTaskId ->
+                if (splitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
+                    SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+                else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+            else ->
+                if (splitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY)
+                    SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+                else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+        }
+    }
+
+    private fun getReadableDuration(
+        duration: Duration,
+        @StringRes durationLessThanOneMinuteStringId: Int,
+    ): String {
+        val hours = Math.toIntExact(duration.toHours())
+        val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes())
+        return when {
+            // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
+            hours > 0 && minutes > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW)
+                    .formatMeasures(
+                        Measure(hours, MeasureUnit.HOUR),
+                        Measure(minutes, MeasureUnit.MINUTE),
+                    )
+            // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
+            hours > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(hours, MeasureUnit.HOUR))
+            // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
+            minutes > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(minutes, MeasureUnit.MINUTE))
+            // Use a specific string for usage less than one minute but non-zero.
+            duration > Duration.ZERO -> context.getString(durationLessThanOneMinuteStringId)
+            // Otherwise, return 0-minute string.
+            else ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(0, MeasureUnit.MINUTE))
+        }
+    }
+
+    /**
+     * Returns text to show for the banner depending on [.getSplitBannerConfig] If {@param
+     * forContentDesc} is `true`, this will always return the full string corresponding to
+     * [.SPLIT_BANNER_FULLSCREEN]
+     */
+    @JvmOverloads
+    @VisibleForTesting
+    fun getBannerText(
+        remainingTime: Long = appRemainingTimeMs,
+        forContentDesc: Boolean = false,
+    ): String {
+        val duration =
+            Duration.ofMillis(
+                if (remainingTime > MINUTE_MS)
+                    (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS
+                else remainingTime
+            )
+        val readableDuration =
+            getReadableDuration(
+                duration,
+                R.string.shorter_duration_less_than_one_minute, /* forceFormatWidth */
+            )
+        val splitBannerConfig = getSplitBannerConfig()
+        return when {
+            forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN ->
+                context.getString(R.string.time_left_for_app, readableDuration)
+            // show no text
+            splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> ""
+            // SPLIT_GRID_BANNER_LARGE only show time
+            else -> readableDuration
+        }
+    }
+
+    private fun openAppUsageSettings(view: View) {
+        val intent =
+            Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, task.topComponent.packageName)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        try {
+            val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
+            context.startActivity(intent, options.toBundle())
+
+            // TODO: add WW logging on the app usage settings click.
+        } catch (e: ActivityNotFoundException) {
+            Log.e(
+                TAG,
+                "Failed to open app usage settings for task " + task.topComponent.packageName,
+                e,
+            )
+        }
+    }
+
+    private fun getContentDescriptionForTask(
+        task: Task,
+        appUsageLimitTimeMs: 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 */),
+            )
+        else task.titleDescription
+
+    fun setupLayout() {
+        val snapshotWidth: Int
+        val snapshotHeight: Int
+        val splitBounds = splitBounds
+        if (splitBounds == null) {
+            snapshotWidth = taskView.layoutParams.width
+            snapshotHeight =
+                taskView.layoutParams.height -
+                    recentsViewContainer.deviceProfile.overviewTaskThumbnailTopMarginPx
+        } else {
+            val groupedTaskSize =
+                taskView.pagedOrientationHandler.getGroupedTaskViewSizes(
+                    recentsViewContainer.deviceProfile,
+                    splitBounds,
+                    taskView.layoutParams.width,
+                    taskView.layoutParams.height,
+                )
+            if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+                snapshotWidth = groupedTaskSize.first.x
+                snapshotHeight = groupedTaskSize.first.y
+            } else {
+                snapshotWidth = groupedTaskSize.second.x
+                snapshotHeight = groupedTaskSize.second.y
+            }
+        }
+        taskView.pagedOrientationHandler.updateDwbBannerLayout(
+            taskView.layoutParams.width,
+            taskView.layoutParams.height,
+            taskView is GroupedTaskView,
+            recentsViewContainer.deviceProfile,
+            snapshotWidth,
+            snapshotHeight,
+            this,
+        )
+    }
+
+    private fun setupTranslations() {
+        val (translationX, translationY) =
+            taskView.pagedOrientationHandler.getDwbBannerTranslations(
+                taskView.layoutParams.width,
+                taskView.layoutParams.height,
+                splitBounds,
+                recentsViewContainer.deviceProfile,
+                taskView.snapshotViews,
+                task.key.id,
+                this,
+            )
+        this.translationX = translationX
+        this.splitOffsetTranslationY = translationY
+    }
+
+    private fun updateTranslationY() {
+        translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
+        invalidateOutline()
+    }
+
+    fun setColorTint(color: Int, amount: Float) {
+        if (amount == 0f) {
+            setLayerType(View.LAYER_TYPE_NONE, null)
+        }
+        val layerPaint = Paint()
+        layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount))
+        setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
+        setLayerPaint(layerPaint)
+    }
+
+    private fun getAccessibilityActionId(): Int =
+        if (splitBounds?.rightBottomTaskId == task.key.id)
+            R.id.action_digital_wellbeing_bottom_right
+        else R.id.action_digital_wellbeing_top_left
+
+    fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? {
+        if (!hasLimit) return null
+        val label =
+            if (taskView.containsMultipleTasks())
+                context.getString(
+                    R.string.split_app_usage_settings,
+                    TaskUtils.getTitle(context, task),
+                )
+            else context.getString(R.string.accessibility_app_usage_settings)
+        return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label)
+    }
+
+    fun handleAccessibilityAction(action: Int): Boolean {
+        if (getAccessibilityActionId() != action) return false
+        openAppUsageSettings(taskView)
+        return true
+    }
+
+    companion object {
+        private const val THRESHOLD_LEFT_ICON_ONLY = 0.4f
+        private const val THRESHOLD_RIGHT_ICON_ONLY = 0.6f
+
+        enum class SplitBannerConfig {
+            /** Will span entire width of taskView with full text */
+            SPLIT_BANNER_FULLSCREEN,
+            /** 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,
+        }
+
+        val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS)
+        const val MINUTE_MS: Int = 60000
+
+        private const val TAG = "DigitalWellBeingToast"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt
new file mode 100644
index 0000000..c893016
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.ViewGroup
+import android.widget.ImageView
+
+/**
+ * An [ImageView] that does not requestLayout() unless setLayoutParams is called.
+ *
+ * This is useful, particularly during animations, for [ImageView]s that are not supposed to be
+ * resized.
+ */
+@SuppressLint("AppCompatCustomView")
+class FixedSizeImageView : ImageView {
+    private var shouldRequestLayoutOnChanges = false
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+    ) : super(context, attrs, defStyleAttr)
+
+    override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
+        shouldRequestLayoutOnChanges = true
+        super.setLayoutParams(params)
+        shouldRequestLayoutOnChanges = false
+    }
+
+    override fun requestLayout() {
+        if (shouldRequestLayoutOnChanges) {
+            super.requestLayout()
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
index 4ea7753..f17be05 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
@@ -32,7 +32,6 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.launcher3.widget.RoundedCornerEnforcement;
 
 import java.util.stream.IntStream;
 
@@ -171,8 +170,7 @@
 
     /** Corner radius from source view's outline, or enforced view. */
     private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) {
-        if (RoundedCornerEnforcement.isRoundedCornerEnabled()
-                && hostView.hasEnforcedCornerRadius()) {
+        if (hostView.hasEnforcedCornerRadius()) {
             return hostView.getEnforcedCornerRadius();
         } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider
                 && v.getClipToOutline()) {
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
index 4dde635..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;
@@ -123,8 +124,8 @@
     @Override
     public void onGlobalLayout() {
         if (isUninitialized()) return;
-        positionViews();
-        if (mOnTargetChangeRunnable != null) {
+        boolean positionsChanged = positionViews();
+        if (mOnTargetChangeRunnable != null && positionsChanged) {
             mOnTargetChangeRunnable.run();
         }
     }
@@ -212,21 +213,43 @@
         onGlobalLayout();
     }
 
-    /** Sets the layout parameters of the floating view and its background view child. */
-    private void positionViews() {
+    /**
+     * Sets the layout parameters of the floating view and its background view child.
+     * @return true if any of the views positions change due to this call.
+     */
+    private boolean positionViews() {
+        boolean positionsChanged = false;
+
         LayoutParams layoutParams = (LayoutParams) getLayoutParams();
-        layoutParams.setMargins(0, 0, 0, 0);
-        setLayoutParams(layoutParams);
+
+        if (layoutParams.topMargin != 0 || layoutParams.bottomMargin != 0
+                || layoutParams.rightMargin != 0 || layoutParams.leftMargin != 0) {
+            positionsChanged = true;
+            layoutParams.setMargins(0, 0, 0, 0);
+            setLayoutParams(layoutParams);
+        }
 
         // FloatingWidgetView layout is forced LTR
-        mBackgroundView.setTranslationX(mBackgroundPosition.left);
-        mBackgroundView.setTranslationY(mBackgroundPosition.top + mIconOffsetY);
+        float targetY = mBackgroundPosition.top + mIconOffsetY;
+        if (mBackgroundView.getTranslationX() != mBackgroundPosition.left
+                || mBackgroundView.getTranslationY() != targetY) {
+            positionsChanged = true;
+            mBackgroundView.setTranslationX(mBackgroundPosition.left);
+            mBackgroundView.setTranslationY(targetY);
+        }
+
         LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams();
-        backgroundParams.leftMargin = 0;
-        backgroundParams.topMargin = 0;
-        backgroundParams.width = (int) mBackgroundPosition.width();
-        backgroundParams.height = (int) mBackgroundPosition.height();
-        mBackgroundView.setLayoutParams(backgroundParams);
+        if (backgroundParams.leftMargin != 0 || backgroundParams.topMargin != 0
+                || backgroundParams.width != Math.round(mBackgroundPosition.width())
+                || backgroundParams.height != Math.round(mBackgroundPosition.height())) {
+            positionsChanged = true;
+
+            backgroundParams.leftMargin = 0;
+            backgroundParams.topMargin = 0;
+            backgroundParams.width = Math.round(mBackgroundPosition.width());
+            backgroundParams.height = Math.round(mBackgroundPosition.height());
+            mBackgroundView.setLayoutParams(backgroundParams);
+        }
 
         if (mForegroundOverlayView != null) {
             sTmpMatrix.reset();
@@ -237,8 +260,15 @@
             sTmpMatrix.postScale(foregroundScale, foregroundScale);
             sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top
                     + mIconOffsetY);
-            mForegroundOverlayView.setMatrix(sTmpMatrix);
+
+            // We use the animation matrix here, because calling setMatrix on the GhostView
+            // actually sets the animation matrix, not the regular one.
+            if (!sTmpMatrix.equals(mForegroundOverlayView.getAnimationMatrix())) {
+                positionsChanged = true;
+                mForegroundOverlayView.setMatrix(sTmpMatrix);
+            }
         }
+        return positionsChanged;
     }
 
     private void finish(DragLayer dragLayer) {
@@ -304,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 6523ba7..92c1e93 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -36,7 +36,7 @@
 import com.android.quickstep.util.SplitSelectStateController
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper
-import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition
+import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition
 
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
@@ -65,31 +65,18 @@
         val heightSize = MeasureSpec.getSize(heightMeasureSpec)
         setMeasuredDimension(widthSize, heightSize)
         val splitBoundsConfig = splitBoundsConfig ?: return
-        val initSplitTaskId = getThisTaskCurrentlyInSplitSelection()
-        if (initSplitTaskId == INVALID_TASK_ID) {
-            pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
-                taskContainers[0].snapshotView,
-                taskContainers[1].snapshotView,
-                widthSize,
-                heightSize,
-                splitBoundsConfig,
-                container.deviceProfile,
-                layoutDirection == LAYOUT_DIRECTION_RTL
-            )
-        } else {
-            // Currently being split with this taskView, let the non-split selected thumbnail
-            // take up full thumbnail area
-            taskContainers
-                .firstOrNull { it.task.key.id != initSplitTaskId }
-                ?.snapshotView
-                ?.measure(
-                    widthMeasureSpec,
-                    MeasureSpec.makeMeasureSpec(
-                        heightSize - container.deviceProfile.overviewTaskThumbnailTopMarginPx,
-                        MeasureSpec.EXACTLY
-                    )
-                )
-        }
+        val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID
+        pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
+            taskContainers[0].snapshotView,
+            taskContainers[1].snapshotView,
+            widthSize,
+            heightSize,
+            splitBoundsConfig,
+            container.deviceProfile,
+            layoutDirection == LAYOUT_DIRECTION_RTL,
+            inSplitSelection,
+        )
+
         if (!enableOverviewIconMenu()) {
             updateIconPlacement()
         }
@@ -115,6 +102,7 @@
                     R.id.snapshot,
                     R.id.icon,
                     R.id.show_windows,
+                    R.id.digital_wellbeing_toast,
                     STAGE_POSITION_TOP_OR_LEFT,
                     taskOverlayFactory
                 ),
@@ -123,6 +111,7 @@
                     R.id.bottomright_snapshot,
                     R.id.bottomRight_icon,
                     R.id.show_windows_right,
+                    R.id.bottomRight_digital_wellbeing_toast,
                     STAGE_POSITION_BOTTOM_OR_RIGHT,
                     taskOverlayFactory
                 )
@@ -130,7 +119,7 @@
         taskContainers.forEach { it.bind() }
 
         this.splitBoundsConfig = splitBoundsConfig
-        taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) }
+        taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig }
         setOrientationState(orientedState)
     }
 
@@ -171,6 +160,8 @@
         val splitBoundsConfig = splitBoundsConfig ?: return
         val taskIconHeight = container.deviceProfile.overviewTaskIconSizePx
         val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+        val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID
+
         if (enableOverviewIconMenu()) {
             val groupedTaskViewSizes =
                 pagedOrientationHandler.getGroupedTaskViewSizes(
@@ -189,7 +180,8 @@
                 layoutParams.width,
                 isRtl,
                 container.deviceProfile,
-                splitBoundsConfig
+                splitBoundsConfig,
+                inSplitSelection
             )
         } else {
             pagedOrientationHandler.setSplitIconParams(
@@ -202,7 +194,8 @@
                 measuredWidth,
                 isRtl,
                 container.deviceProfile,
-                splitBoundsConfig
+                splitBoundsConfig,
+                inSplitSelection
             )
         }
     }
@@ -210,17 +203,13 @@
     fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) {
         splitBoundsConfig = splitBounds
         taskContainers.forEach {
-            it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig)
-            it.digitalWellBeingToast?.initialize(it.task)
+            it.digitalWellBeingToast?.splitBounds = splitBoundsConfig
+            it.digitalWellBeingToast?.initialize()
         }
         invalidate()
     }
 
-    override fun launchTaskAnimated(): RunnableList? {
-        if (taskContainers.isEmpty()) {
-            Log.d(TAG, "launchTaskAnimated - task is not bound")
-            return null
-        }
+    override fun launchAsStaticTile(): RunnableList? {
         val recentsView = recentsView ?: return null
         val endCallback = RunnableList()
         // Callbacks run from remote animation when recents animation not currently running
@@ -239,8 +228,11 @@
         return endCallback
     }
 
-    override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
-        launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/)
+    override fun launchWithoutAnimation(
+        isQuickSwitch: Boolean,
+        callback: (launched: Boolean) -> Unit
+    ) {
+        launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback)
     }
 
     /**
@@ -264,7 +256,10 @@
                 isQuickSwitch,
                 snapPosition
             )
-            Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}")
+            Log.d(
+                TAG,
+                "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView"
+            )
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
deleted file mode 100644
index bb4a7ec..0000000
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
- * when the drawable changes.
- */
-public class IconView extends View implements TaskViewIcon {
-    private static final int NUM_ALPHA_CHANNELS = 2;
-    private static final int INDEX_CONTENT_ALPHA = 0;
-    private static final int INDEX_MODAL_ALPHA = 1;
-
-    private final MultiValueAlpha mMultiValueAlpha;
-
-    @Nullable
-    private Drawable mDrawable;
-    private int mDrawableWidth, mDrawableHeight;
-
-    public IconView(Context context) {
-        this(context, null);
-    }
-
-    public IconView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
-        mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
-    }
-
-    /**
-     * Sets a {@link Drawable} to be displayed.
-     */
-    @Override
-    public void setDrawable(@Nullable Drawable d) {
-        if (mDrawable != null) {
-            mDrawable.setCallback(null);
-        }
-        mDrawable = d;
-        if (mDrawable != null) {
-            mDrawable.setCallback(this);
-            setDrawableSizeInternal(getWidth(), getHeight());
-        }
-        invalidate();
-    }
-
-    /**
-     * Sets the size of the icon drawable.
-     */
-    @Override
-    public void setDrawableSize(int iconWidth, int iconHeight) {
-        mDrawableWidth = iconWidth;
-        mDrawableHeight = iconHeight;
-        if (mDrawable != null) {
-            setDrawableSizeInternal(getWidth(), getHeight());
-        }
-    }
-
-    private void setDrawableSizeInternal(int selfWidth, int selfHeight) {
-        Rect selfRect = new Rect(0, 0, selfWidth, selfHeight);
-        Rect drawableRect = new Rect();
-        Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect);
-        mDrawable.setBounds(drawableRect);
-    }
-
-    @Override
-    @Nullable
-    public Drawable getDrawable() {
-        return mDrawable;
-    }
-
-    @Override
-    public int getDrawableWidth() {
-        return mDrawableWidth;
-    }
-
-    @Override
-    public int getDrawableHeight() {
-        return mDrawableHeight;
-    }
-
-    @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-        if (mDrawable != null) {
-            setDrawableSizeInternal(w, h);
-        }
-    }
-
-    @Override
-    protected boolean verifyDrawable(Drawable who) {
-        return super.verifyDrawable(who) || who == mDrawable;
-    }
-
-    @Override
-    protected void drawableStateChanged() {
-        super.drawableStateChanged();
-
-        final Drawable drawable = mDrawable;
-        if (drawable != null && drawable.isStateful()
-                && drawable.setState(getDrawableState())) {
-            invalidateDrawable(drawable);
-        }
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        if (mDrawable != null) {
-            mDrawable.draw(canvas);
-        }
-    }
-
-    @Override
-    public boolean hasOverlappingRendering() {
-        return false;
-    }
-
-    @Override
-    public void setContentAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
-    }
-
-    @Override
-    public void setModalAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
-    }
-
-    /**
-     * Set the tint color of the icon, useful for scrimming or dimming.
-     *
-     * @param color to blend in.
-     * @param amount [0,1] 0 no tint, 1 full tint
-     */
-    @Override
-    public void setIconColorTint(int color, float amount) {
-        if (mDrawable != null) {
-            mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
-        }
-    }
-
-    @Override
-    public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
-        RecentsPagedOrientationHandler orientationHandler =
-                orientationState.getOrientationHandler();
-        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        DeviceProfile deviceProfile =
-                ActivityContext.lookupContext(getContext()).getDeviceProfile();
-
-        FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
-
-        int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
-        int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
-        int taskMargin = deviceProfile.overviewTaskMarginPx;
-
-        orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
-                thumbnailTopMargin, isRtl);
-        iconParams.width = iconParams.height = taskIconHeight;
-        setLayoutParams(iconParams);
-
-        setRotation(orientationHandler.getDegreesRotated());
-        int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
-                : deviceProfile.overviewTaskIconDrawableSizePx;
-        setDrawableSize(iconDrawableSize, iconDrawableSize);
-    }
-
-    @Override
-    public View asView() {
-        return this;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
new file mode 100644
index 0000000..583207f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
+ * when the drawable changes.
+ */
+class IconView : View, TaskViewIcon {
+    private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+    private var drawable: Drawable? = null
+    private var drawableWidth = 0
+    private var drawableHeight = 0
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+    ) : super(context, attrs, defStyleAttr)
+
+    init {
+        multiValueAlpha.setUpdateVisibility(true)
+    }
+
+    /** Sets a [Drawable] to be displayed. */
+    override fun setDrawable(d: Drawable?) {
+        drawable?.callback = null
+
+        drawable = d
+        drawable?.let {
+            it.callback = this
+            setDrawableSizeInternal(width, height)
+        }
+        invalidate()
+    }
+
+    /** Sets the size of the icon drawable. */
+    override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+        drawableWidth = iconWidth
+        drawableHeight = iconHeight
+        drawable?.let { setDrawableSizeInternal(width, height) }
+    }
+
+    private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) {
+        val selfRect = Rect(0, 0, selfWidth, selfHeight)
+        val drawableRect = Rect()
+        Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect)
+        drawable?.bounds = drawableRect
+    }
+
+    override fun getDrawable(): Drawable? = drawable
+
+    override fun getDrawableWidth(): Int = drawableWidth
+
+    override fun getDrawableHeight(): Int = drawableHeight
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        drawable?.let { setDrawableSizeInternal(w, h) }
+    }
+
+    override fun verifyDrawable(who: Drawable): Boolean =
+        super.verifyDrawable(who) || who === drawable
+
+    override fun drawableStateChanged() {
+        super.drawableStateChanged()
+        drawable?.let {
+            if (it.isStateful && it.setState(drawableState)) {
+                invalidateDrawable(it)
+            }
+        }
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        drawable?.draw(canvas)
+    }
+
+    override fun hasOverlappingRendering(): Boolean = false
+
+    override fun setContentAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha)
+    }
+
+    override fun setModalAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha)
+    }
+
+    /**
+     * Set the tint color of the icon, useful for scrimming or dimming.
+     *
+     * @param color to blend in.
+     * @param amount [0,1] 0 no tint, 1 full tint
+     */
+    override fun setIconColorTint(color: Int, amount: Float) {
+        drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount)
+    }
+
+    override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+        val orientationHandler = orientationState.orientationHandler
+        val deviceProfile: DeviceProfile =
+            (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile()
+        orientationHandler.setTaskIconParams(
+            iconParams = getLayoutParams() as FrameLayout.LayoutParams,
+            taskIconMargin = deviceProfile.overviewTaskMarginPx,
+            taskIconHeight = deviceProfile.overviewTaskIconSizePx,
+            thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx,
+            isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+        )
+        updateLayoutParams<FrameLayout.LayoutParams> {
+            height = deviceProfile.overviewTaskIconSizePx
+            width = height
+        }
+        setRotation(orientationHandler.degreesRotated)
+        val iconDrawableSize =
+            if (isGridTask) deviceProfile.overviewTaskIconDrawableSizeGridPx
+            else deviceProfile.overviewTaskIconDrawableSizePx
+        setDrawableSize(iconDrawableSize, iconDrawableSize)
+    }
+
+    override fun asView(): View = this
+
+    companion object {
+        private const val NUM_ALPHA_CHANNELS = 2
+        private const val INDEX_CONTENT_ALPHA = 0
+        private const val INDEX_MODAL_ALPHA = 1
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index e48a7c6..bbb8cc8 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -16,17 +16,14 @@
 package com.android.quickstep.views;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+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 static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -54,6 +51,7 @@
 import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.systemui.shared.recents.model.Task;
 
@@ -91,10 +89,12 @@
     protected void handleStartHome(boolean animated) {
         StateManager stateManager = getStateManager();
         animated &= stateManager.shouldAnimateStateChange();
-        stateManager.goToState(NORMAL, animated);
-        if (FeatureFlags.enableSplitContextually()) {
-            mSplitSelectStateController.getSplitAnimationController()
-                    .playPlaceholderDismissAnim(mContainer, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
+        if (mSplitSelectStateController.isSplitSelectActive()) {
+            AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+                    LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+                    mSplitSelectStateController.getSplitAnimationController());
+        } else {
+            stateManager.goToState(NORMAL, animated);
         }
         AbstractFloatingView.closeAllOpenViews(mContainer, animated);
     }
@@ -170,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();
         }
@@ -251,7 +250,7 @@
     }
 
     @Override
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         if (FeatureFlags.enableSplitContextually()) {
             return !mSplitSelectStateController.isSplitSelectActive();
         } else {
@@ -265,7 +264,8 @@
         super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
         DesktopVisibilityController desktopVisibilityController =
                 mContainer.getDesktopVisibilityController();
-        if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                && desktopVisibilityController != null) {
             // TODO: b/333533253 - Remove after flag rollout
             desktopVisibilityController.setRecentsGestureStart();
         }
@@ -288,7 +288,8 @@
             }
         }
         super.onGestureAnimationEnd();
-        if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                && desktopVisibilityController != null) {
             // TODO: b/333533253 - Remove after flag rollout
             desktopVisibilityController.setRecentsGestureEnd(endTarget);
         }
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index d9468c7..4a2be2a 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -274,7 +274,9 @@
     }
 
     private void updateActionButtonsVisibility() {
-        assert mDp != null;
+        if (mDp == null) {
+            return;
+        }
         boolean showSingleTaskActions = !mIsGroupedTask;
         boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair;
         Log.d(TAG, "updateActionButtonsVisibility() called: showSingleTaskActions = ["
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 3273809..3a4e328 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -36,10 +36,13 @@
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
+import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation;
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
@@ -57,6 +60,7 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
+import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
 import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN;
@@ -90,6 +94,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BlendMode;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -124,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;
@@ -135,6 +141,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.PagedView;
@@ -191,16 +198,22 @@
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.recents.data.TasksRepository;
+import com.android.quickstep.recents.data.RecentTasksRepository;
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository;
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl;
+import com.android.quickstep.recents.data.RecentsRotationStateRepository;
+import com.android.quickstep.recents.data.RecentsRotationStateRepositoryImpl;
+import com.android.quickstep.recents.di.RecentsDependencies;
 import com.android.quickstep.recents.viewmodel.RecentsViewData;
-import com.android.quickstep.util.ActiveGestureErrorDetector;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.recents.viewmodel.RecentsViewModel;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsAtomicAnimationFactory;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.RecentsViewUtils;
 import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps;
 import com.android.quickstep.util.SplitAnimationTimings;
 import com.android.quickstep.util.SplitSelectStateController;
@@ -219,14 +232,15 @@
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
 
 import kotlin.Unit;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -239,11 +253,12 @@
 
 /**
  * A list of recent tasks.
+ *
  * @param <CONTAINER_TYPE> : the container that should host recents view
- * @param <STATE_TYPE> : the type of base state that will be used
+ * @param <STATE_TYPE>     : the type of base state that will be used
  */
-
-public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewContainer,
+public abstract class RecentsView<
+        CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
         TaskVisualsChangeListener {
@@ -306,8 +321,22 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> RUNNING_TASK_ATTACH_ALPHA =
+            new FloatProperty<RecentsView>("runningTaskAttachAlpha") {
+                @Override
+                public void setValue(RecentsView recentsView, float v) {
+                    recentsView.mRunningTaskAttachAlpha = v;
+                    recentsView.applyAttachAlpha();
+                }
+
+                @Override
+                public Float get(RecentsView recentsView) {
+                    return recentsView.mRunningTaskAttachAlpha;
+                }
+            };
+
     public static final int SCROLL_VIBRATION_PRIMITIVE =
-            Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1;
+            VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
     public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f;
     public static final VibrationEffect SCROLL_VIBRATION_FALLBACK =
             VibrationConstants.EFFECT_TEXTURE_TICK;
@@ -389,7 +418,7 @@
                     view.setScaleX(scale);
                     view.setScaleY(scale);
                     if (enableRefactorTaskThumbnail()) {
-                        view.mRecentsViewData.getScale().setValue(scale);
+                        view.mRecentsViewModel.updateScale(scale);
                     }
                     view.mLastComputedTaskStartPushOutDistance = null;
                     view.mLastComputedTaskEndPushOutDistance = null;
@@ -428,6 +457,21 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> DESKTOP_CAROUSEL_DETACH_PROGRESS =
+            new FloatProperty<>("desktopCarouselDetachProgress") {
+                @Override
+                public void setValue(RecentsView view, float offset) {
+                    view.mDesktopCarouselDetachProgress = offset;
+                    view.applyAttachAlpha();
+                    view.updatePageOffsets();
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.mDesktopCarouselDetachProgress;
+                }
+            };
+
     /**
      * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and
      * being in any other state has a value of 0.
@@ -461,11 +505,6 @@
 
     private static final float FOREGROUND_SCRIM_TINT = 0.32f;
 
-    @Nullable
-    public final RecentsViewData mRecentsViewData = new RecentsViewData();
-    @Nullable
-    public final TasksRepository mTasksRepository;
-
     protected final RecentsOrientedState mOrientationState;
     protected final BaseContainerInterface<STATE_TYPE, CONTAINER_TYPE> mSizeStrategy;
     @Nullable
@@ -544,12 +583,14 @@
     private int mClampedScrollOffsetBound;
 
     private float mAdjacentPageHorizontalOffset = 0;
+    private float mDesktopCarouselDetachProgress = 0;
     protected float mTaskViewsSecondaryTranslation = 0;
     protected float mTaskViewsPrimarySplitTranslation = 0;
     protected float mTaskViewsSecondarySplitTranslation = 0;
     // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
     private float mGridProgress = 0;
     private float mTaskThumbnailSplashAlpha = 0;
+    private boolean mBorderEnabled = false;
     private boolean mShowAsGridLastOnLayout = false;
     private final IntSet mTopRowIdSet = new IntSet();
     private int mClearAllShortTotalWidthTranslation = 0;
@@ -603,13 +644,16 @@
         @Override
         public void onTaskRemoved(int taskId) {
             if (!mHandleTaskStackChanges) {
+                Log.d(TAG, "onTaskRemoved: " + taskId + ", not handling task stack changes");
                 return;
             }
 
             TaskView taskView = getTaskViewByTaskId(taskId);
             if (taskView == null) {
+                Log.d(TAG, "onTaskRemoved: " + taskId + ", no associated TaskView");
                 return;
             }
+            Log.d(TAG, "onTaskRemoved: " + taskId);
             Task.TaskKey taskKey = taskView.getFirstTask().key;
             UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
                     () -> PackageManagerWrapper.getInstance()
@@ -648,10 +692,11 @@
     protected boolean mRunningTaskTileHidden;
     @Nullable
     private Task[] mTmpRunningTasks;
-    protected int mFocusedTaskViewId = -1;
+    protected int mFocusedTaskViewId = INVALID_TASK_ID;
 
     private boolean mTaskIconScaledDown = false;
     private boolean mRunningTaskShowScreenshot = false;
+    private float mRunningTaskAttachAlpha;
 
     private boolean mOverviewStateEnabled;
     private boolean mHandleTaskStackChanges;
@@ -720,10 +765,12 @@
 
     private final SplitSelectionListener mSplitSelectionListener = new SplitSelectionListener() {
         @Override
-        public void onSplitSelectionConfirmed() { }
+        public void onSplitSelectionConfirmed() {
+        }
 
         @Override
-        public void onSplitSelectionActive() { }
+        public void onSplitSelectionActive() {
+        }
 
         @Override
         public void onSplitSelectionExit(boolean launchedSplit) {
@@ -761,12 +808,6 @@
     @Nullable
     private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
 
-    /**
-     * Keeps track of the desktop task. Optional and only present when the feature flag is enabled.
-     */
-    @Nullable
-    private DesktopTaskView mDesktopTaskView;
-
     private MultiWindowModeChangedListener mMultiWindowModeChangedListener =
             new MultiWindowModeChangedListener() {
                 @Override
@@ -802,10 +843,15 @@
      */
     private boolean mAnyTaskHasBeenDismissed;
 
+    private final RecentsViewModel mRecentsViewModel;
+    private final RecentsViewModelHelper mHelper;
+    private final RecentsViewUtils mUtils = new RecentsViewUtils();
+
     public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             BaseContainerInterface sizeStrategy) {
         super(context, attrs, defStyleAttr);
         setEnableFreeScroll(true);
+
         mSizeStrategy = sizeStrategy;
         mContainer = RecentsViewContainer.containerFromContext(context);
         mOrientationState = new RecentsOrientedState(
@@ -813,18 +859,32 @@
         final int rotation = mContainer.getDisplay().getRotation();
         mOrientationState.setRecentsRotation(rotation);
 
+        // Start Recents Dependency graph
+        if (enableRefactorTaskThumbnail()) {
+            RecentsDependencies recentsDependencies = RecentsDependencies.Companion.initialize(
+                    this);
+            mRecentsViewModel = new RecentsViewModel(
+                    recentsDependencies.inject(RecentTasksRepository.class),
+                    recentsDependencies.inject(RecentsViewData.class)
+            );
+            mHelper = new RecentsViewModelHelper(mRecentsViewModel);
+
+            recentsDependencies.provide(RecentsRotationStateRepository.class,
+                    () -> new RecentsRotationStateRepositoryImpl(mOrientationState));
+
+            recentsDependencies.provide(RecentsDeviceProfileRepository.class,
+                    () -> new RecentsDeviceProfileRepositoryImpl(mContainer));
+        } else {
+            mRecentsViewModel = null;
+            mHelper = null;
+        }
+
         mScrollHapticMinGapMillis = getResources()
                 .getInteger(R.integer.recentsScrollHapticMinGapMillis);
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
-        if (enableRefactorTaskThumbnail()) {
-            mTasksRepository = new TasksRepository(
-                    mModel, mModel.getThumbnailCache(), mModel.getIconCache());
-        } else {
-            mTasksRepository = null;
-        }
 
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
@@ -1024,8 +1084,10 @@
     @Override
     @Nullable
     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
+        if (enableRefactorTaskThumbnail()) {
+            return null;
+        }
         if (mHandleTaskStackChanges) {
-            // TODO(b/342560598): Handle onTaskThumbnailChanged for new TTV.
             if (!enableRefactorTaskThumbnail()) {
                 TaskView taskView = getTaskViewByTaskId(taskId);
                 if (taskView != null) {
@@ -1043,15 +1105,14 @@
     }
 
     @Override
-    public void onTaskIconChanged(String pkg, UserHandle user) {
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView tv = requireTaskViewAt(i);
-            Task task = tv.getFirstTask();
+    public void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {
+        for (TaskView taskView : getTaskViews()) {
+            Task task = taskView.getFirstTask();
             if (pkg.equals(task.key.getPackageName()) && task.key.userId == user.getIdentifier()) {
                 task.icon = null;
-                if (tv.getTaskContainers().stream().anyMatch(
+                if (taskView.getTaskContainers().stream().anyMatch(
                         container -> container.getIconView().getDrawable() != null)) {
-                    tv.onTaskListVisibilityChanged(true /* visible */);
+                    taskView.onTaskListVisibilityChanged(true /* visible */);
                 }
             }
         }
@@ -1059,46 +1120,36 @@
 
     @Override
     public void onTaskIconChanged(int taskId) {
+        if (enableRefactorTaskThumbnail()) {
+            return;
+        }
         TaskView taskView = getTaskViewByTaskId(taskId);
         if (taskView != null) {
             taskView.refreshTaskThumbnailSplash();
         }
     }
 
-    /**
-     * Update the thumbnail(s) of the relevant TaskView.
-     * @param refreshNow Refresh immediately if it's true.
-     */
-    @Nullable
-    public TaskView updateThumbnail(
-            HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow) {
-        if (enableRefactorTaskThumbnail()) {
-            // TODO(b/342560598): Handle updateThumbnail for new TTV.
-            return null;
-        }
-        TaskView updatedTaskView = null;
-        for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
-            Integer id = entry.getKey();
-            ThumbnailData thumbnail = entry.getValue();
-            TaskView taskView = getTaskViewByTaskId(id);
-            if (taskView == null) {
-                continue;
+    /** Updates the thumbnail(s) of the relevant TaskView. */
+    public void updateThumbnail(Map<Integer, ThumbnailData> thumbnailData) {
+        if (!enableRefactorTaskThumbnail()) {
+            for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
+                Integer id = entry.getKey();
+                ThumbnailData thumbnail = entry.getValue();
+                TaskView taskView = getTaskViewByTaskId(id);
+                if (taskView == null) {
+                    continue;
+                }
+                // taskView could be a GroupedTaskView, so select the relevant task by ID
+                TaskContainer taskContainer = taskView.getTaskContainerById(id);
+                if (taskContainer == null) {
+                    continue;
+                }
+                Task task = taskContainer.getTask();
+                TaskThumbnailViewDeprecated taskThumbnailViewDeprecated =
+                        taskContainer.getThumbnailViewDeprecated();
+                taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, /*refreshNow=*/false);
             }
-            // taskView could be a GroupedTaskView, so select the relevant task by ID
-            TaskContainer taskAttributes = taskView.getTaskContainerById(id);
-            if (taskAttributes == null) {
-                continue;
-            }
-            Task task = taskAttributes.getTask();
-            TaskThumbnailViewDeprecated taskThumbnailViewDeprecated =
-                    taskAttributes.getThumbnailViewDeprecated();
-            taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, refreshNow);
-            // thumbnailData can contain 1-2 ids, but they should correspond to the same
-            // TaskView, so overwriting is ok
-            updatedTaskView = taskView;
         }
-
-        return updatedTaskView;
     }
 
     @Override
@@ -1127,9 +1178,10 @@
 
     /**
      * See overridden implementations
+     *
      * @return {@code true} if child TaskViews can be launched when user taps on them
      */
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         return true;
     }
 
@@ -1152,11 +1204,15 @@
         if (FeatureFlags.enableSplitContextually()) {
             mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
         }
+        if (enableRefactorTaskThumbnail()) {
+            mHelper.onAttachedToWindow();
+        }
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
+
         updateTaskStackListenerState();
         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
         mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
@@ -1174,6 +1230,9 @@
             mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
         }
         reset();
+        if (enableRefactorTaskThumbnail()) {
+            mHelper.onDetachedFromWindow();
+        }
     }
 
     @Override
@@ -1186,21 +1245,24 @@
         // - It's the focused task to be moved to the front, we immediately re-add the task
         if (child instanceof TaskView && child != mSplitHiddenTaskView
                 && child != mMovingTaskView) {
-            TaskView taskView = (TaskView) child;
-            for (int i : taskView.getTaskIds()) {
-                mHasVisibleTaskData.delete(i);
-            }
-            if (child instanceof GroupedTaskView) {
-                mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
-            } else if (child instanceof DesktopTaskView) {
-                mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
-            } else {
-                mTaskViewPool.recycle(taskView);
-            }
-            mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+            clearAndRecycleTaskView((TaskView) child);
         }
     }
 
+    private void clearAndRecycleTaskView(TaskView taskView) {
+        for (int taskId : taskView.getTaskIds()) {
+            mHasVisibleTaskData.delete(taskId);
+        }
+        if (taskView instanceof GroupedTaskView) {
+            mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
+        } else if (taskView instanceof DesktopTaskView) {
+            mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
+        } else {
+            mTaskViewPool.recycle(taskView);
+        }
+        mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+    }
+
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
@@ -1443,8 +1505,7 @@
             return null;
         }
 
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             if (taskView.containsTaskId(taskId)) {
                 return taskView;
             }
@@ -1465,8 +1526,7 @@
         int[] taskIdsCopy = Arrays.copyOf(taskIds, taskIds.length);
         Arrays.sort(taskIdsCopy);
 
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             int[] taskViewIdsCopy = taskView.getTaskIds();
             Arrays.sort(taskViewIdsCopy);
             if (Arrays.equals(taskIdsCopy, taskViewIdsCopy)) {
@@ -1501,9 +1561,8 @@
      * Enable or disable showing border on hover and focus change on task views
      */
     public void setTaskBorderEnabled(boolean enabled) {
-        int taskCount = getTaskViewCount();
-        for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        mBorderEnabled = enabled;
+        for (TaskView taskView : getTaskViews()) {
             taskView.setBorderEnabled(enabled);
         }
         mClearAllButton.setBorderEnabled(enabled);
@@ -1534,8 +1593,7 @@
     @Override
     protected void onPageEndTransition() {
         super.onPageEndTransition();
-        ActiveGestureLog.INSTANCE.addLog(
-                "onPageEndTransition: current page index updated", getNextPage());
+        ActiveGestureProtoLogProxy.logOnPageEndTransition(getNextPage());
         if (isClearAllHidden() && !mContainer.getDeviceProfile().isTablet) {
             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
         }
@@ -1570,9 +1628,7 @@
         super.onTouchEvent(ev);
 
         if (showAsGrid()) {
-            int taskCount = getTaskViewCount();
-            for (int i = 0; i < taskCount; i++) {
-                TaskView taskView = requireTaskViewAt(i);
+            for (TaskView taskView : getTaskViews()) {
                 if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
                     // Keep consuming events to pass to delegate
                     return true;
@@ -1654,11 +1710,12 @@
                     return;
                 }
                 TaskView taskView = getTaskViewAt(mNextPage);
-                // Snap to fully visible focused task and clear all button.
-                boolean shouldSnapToFocusedTask = taskView != null && taskView.isFocusedTask()
-                        && isTaskViewFullyVisible(taskView);
+                boolean shouldSnapToLargeTask = taskView != null && taskView.isLargeTile()
+                        && !mUtils.isAnySmallTaskFullyVisible(getTaskViews(),
+                        this::isTaskViewFullyVisible);
                 boolean shouldSnapToClearAll = mNextPage == indexOfChild(mClearAllButton);
-                if (!shouldSnapToFocusedTask && !shouldSnapToClearAll) {
+                // Snap to large tile when grid tasks aren't fully visible or the clear all button.
+                if (!shouldSnapToLargeTask && !shouldSnapToClearAll) {
                     return;
                 }
             }
@@ -1718,7 +1775,9 @@
             return;
         }
 
-        if (mCurrentPage == 0) {
+        int frontIndex = enableLargeDesktopWindowingTile() ? getDesktopTaskViewCount() : 0;
+
+        if (mCurrentPage <= frontIndex) {
             return;
         }
 
@@ -1730,16 +1789,16 @@
         removeView(runningTaskView);
         mMovingTaskView = null;
         runningTaskView.resetPersistentViewTransforms();
-        addView(runningTaskView, 0);
-        setCurrentPage(0);
+
+        addView(runningTaskView, frontIndex);
+        setCurrentPage(frontIndex);
 
         updateTaskSize();
     }
 
     @Override
     protected void onScrollerAnimationAborted() {
-        ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
-                ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+        ActiveGestureProtoLogProxy.logOnScrollerAnimationAborted();
     }
 
     @Override
@@ -1749,7 +1808,8 @@
 
     protected void applyLoadPlan(List<GroupTask> taskGroups) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups));
+            final List<GroupTask> finalTaskGroups = taskGroups;
+            mPendingAnimation.addEndListener(success -> applyLoadPlan(finalTaskGroups));
             return;
         }
 
@@ -1794,12 +1854,15 @@
 
         // Reset the focused task to avoiding initializing TaskViews layout as focused task during
         // binding. The focused task view will be updated after all the TaskViews are bound.
-        mFocusedTaskViewId = INVALID_TASK_ID;
+        setFocusedTaskViewId(INVALID_TASK_ID);
 
         // Removing views sets the currentPage to 0, so we save this and restore it after
         // the new set of views are added
         int previousCurrentPage = mCurrentPage;
         int previousFocusedPage = indexOfChild(getFocusedChild());
+        // TaskIds will no longer be valid after remove and re-add, clearing mTopRowIdSet.
+        mAnyTaskHasBeenDismissed = false;
+        mTopRowIdSet.clear();
         removeAllViews();
 
         // If we are entering Overview as a result of initiating a split from somewhere else
@@ -1815,25 +1878,32 @@
         mFilterState.updateInstanceCountMap(taskGroups);
 
         // Clear out desktop view if it is set
-        mDesktopTaskView = null;
+
+        // Move Desktop Tasks to the end of the list
+        if (enableLargeDesktopWindowingTile()) {
+            taskGroups = mUtils.sortDesktopTasksToFront(taskGroups);
+        }
 
         // Add views as children based on whether it's grouped or single task. Looping through
         // 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;
@@ -1849,7 +1919,6 @@
                                 .toList();
                 ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState,
                         mTaskOverlayFactory);
-                mDesktopTaskView = (DesktopTaskView) taskView;
             } else {
                 Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
                         : groupTask.task1;
@@ -1869,12 +1938,18 @@
 
         // Keep same previous focused task
         TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
-        // If the list changed, maybe the focused task doesn't exist anymore
-        if (newFocusedTaskView == null && getTaskViewCount() > 0) {
-            newFocusedTaskView = getTaskViewAt(0);
+        if (enableLargeDesktopWindowingTile() && newFocusedTaskView instanceof DesktopTaskView) {
+            newFocusedTaskView = null;
         }
-        mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview()
-                ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID;
+        // If the list changed, maybe the focused task doesn't exist anymore
+        int newFocusedTaskViewIndex = mUtils.getFocusedTaskIndex(taskGroups);
+        if (newFocusedTaskView == null && getTaskViewCount() > newFocusedTaskViewIndex) {
+            newFocusedTaskView = getTaskViewAt(newFocusedTaskViewIndex);
+        }
+
+        setFocusedTaskViewId(newFocusedTaskView != null && !enableGridOnlyOverview()
+                ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID);
+
         updateTaskSize();
         updateChildTaskOrientations();
 
@@ -1914,8 +1989,8 @@
             // Set the current page to the running task, but not if settling on new task.
             if (hasAllValidTaskIds(runningTaskIds)) {
                 targetPage = indexOfChild(newRunningTaskView);
-            } else if (getTaskViewCount() > 0) {
-                targetPage = indexOfChild(requireTaskViewAt(0));
+            } else if (getTaskViewCount() > newFocusedTaskViewIndex) {
+                targetPage = indexOfChild(requireTaskViewAt(newFocusedTaskViewIndex));
             }
         }
         if (targetPage != -1 && mCurrentPage != targetPage) {
@@ -1940,6 +2015,7 @@
             // generally map to the same task.
             mIgnoreResetTaskId = INVALID_TASK_ID;
         }
+
         resetTaskVisuals();
         onTaskStackUpdated();
         updateEnabledOverlays();
@@ -1957,15 +2033,13 @@
     }
 
     private void removeTasksViewsAndClearAllButton() {
-        // This handles an edge case where applyLoadPlan happens during a gesture when the
-        // only Task is one with excludeFromRecents, in which case we should not remove it.
-        final int stubRunningTaskIndex = isGestureActive() ? getRunningTaskIndex() : -1;
-
-        for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            if (i == stubRunningTaskIndex) {
+        for (TaskView taskView : getTaskViews()) {
+            if (isGestureActive() && taskView.isRunningTask()) {
+                // This handles an edge case where applyLoadPlan happens during a gesture when the
+                // only Task is one with excludeFromRecents, in which case we should not remove it.
                 continue;
             }
-            removeView(requireTaskViewAt(i));
+            removeView(taskView);
         }
         if (getTaskViewCount() == 0 && indexOfChild(mClearAllButton) != -1) {
             removeView(mClearAllButton);
@@ -1980,14 +2054,13 @@
         return taskViewCount;
     }
 
-    public int getGroupedTaskViewCount() {
-        int groupViewCount = 0;
-        for (int i = 0; i < getChildCount(); i++) {
-            if (getChildAt(i) instanceof GroupedTaskView) {
-                groupViewCount++;
-            }
-        }
-        return groupViewCount;
+    /**
+     * Transverse RecentsView children to calculate the amount of DesktopTaskViews.
+     *
+     * @return Number of children that are instances of DesktopTaskView
+     */
+    private int getDesktopTaskViewCount() {
+        return mUtils.getDesktopTaskViewCount(getTaskViews());
     }
 
     /**
@@ -2010,8 +2083,7 @@
     }
 
     public void resetTaskVisuals() {
-        for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             if (Arrays.stream(taskView.getTaskIds()).noneMatch(
                     taskId -> taskId == mIgnoreResetTaskId)) {
                 taskView.resetViewTransforms();
@@ -2020,6 +2092,7 @@
                 taskView.setFullscreenProgress(mFullscreenProgress);
                 taskView.setModalness(mTaskModalness);
                 taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha);
+                taskView.setBorderEnabled(mBorderEnabled);
             }
         }
         // resetTaskVisuals is called at the end of dismiss animation which could update
@@ -2032,14 +2105,10 @@
             simulator.fullScreenProgress.value = 0;
             simulator.recentsViewScale.value = 1;
         });
-        // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
-        // null.
-        if (!mRunningTaskShowScreenshot) {
-            setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
-        }
-        if (mRunningTaskTileHidden) {
-            setRunningTaskHidden(mRunningTaskTileHidden);
-        }
+        // Reapply runningTask related attributes as they might have been reset by
+        // resetViewTransforms().
+        setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
+        applyAttachAlpha();
 
         updateCurveProperties();
         // Update the set of visible task's data
@@ -2051,11 +2120,10 @@
     public void setFullscreenProgress(float fullscreenProgress) {
         mFullscreenProgress = fullscreenProgress;
         if (enableRefactorTaskThumbnail()) {
-            mRecentsViewData.getFullscreenProgress().setValue(mFullscreenProgress);
+            mRecentsViewModel.updateFullscreenProgress(mFullscreenProgress);
         }
-        int taskCount = getTaskViewCount();
-        for (int i = 0; i < taskCount; i++) {
-            requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
+        for (TaskView taskView : getTaskViews()) {
+            taskView.setFullscreenProgress(mFullscreenProgress);
         }
         mClearAllButton.setFullscreenProgress(fullscreenProgress);
 
@@ -2068,6 +2136,7 @@
         boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
                 && getWindowVisibility() == VISIBLE;
         if (handleTaskStackChanges != mHandleTaskStackChanges) {
+            Log.d(TAG, "updateTaskStackListenerState: " + handleTaskStackChanges);
             mHandleTaskStackChanges = handleTaskStackChanges;
             if (handleTaskStackChanges) {
                 reloadIfNeeded();
@@ -2200,8 +2269,7 @@
                     ? mLastComputedCarouselTaskSize.right - mLastComputedTaskSize.right
                     : mLastComputedCarouselTaskSize.left - mLastComputedTaskSize.left;
         }
-        for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             taskView.updateTaskSize(mLastComputedTaskSize, mLastComputedGridTaskSize,
                     mLastComputedCarouselTaskSize);
             taskView.setNonGridTranslationX(accumulatedTranslationX);
@@ -2409,10 +2477,6 @@
                 List<Task> tasksToUpdate = containers.stream()
                         .map(TaskContainer::getTask)
                         .collect(Collectors.toCollection(ArrayList::new));
-                if (enableRefactorTaskThumbnail()) {
-                    visibleTaskIds.addAll(
-                            tasksToUpdate.stream().map((task) -> task.key.id).toList());
-                }
                 if (mTmpRunningTasks != null) {
                     for (Task t : mTmpRunningTasks) {
                         // Skip loading if this is the task that we are animating into
@@ -2420,6 +2484,10 @@
                         tasksToUpdate.removeIf(task -> task == t);
                     }
                 }
+                if (enableRefactorTaskThumbnail()) {
+                    visibleTaskIds.addAll(
+                            tasksToUpdate.stream().map((task) -> task.key.id).toList());
+                }
                 if (tasksToUpdate.isEmpty()) {
                     continue;
                 }
@@ -2449,7 +2517,7 @@
             }
         }
         if (enableRefactorTaskThumbnail()) {
-            mTasksRepository.setVisibleTasks(visibleTaskIds);
+            mRecentsViewModel.updateVisibleTasks(visibleTaskIds);
         }
     }
 
@@ -2476,6 +2544,10 @@
             mModel.preloadCacheIfNeeded();
         }
 
+        if (enableRefactorTaskThumbnail()) {
+            return;
+        }
+
         // Whenever the high res loading state changes, poke each of the visible tasks to see if
         // they want to updated their thumbnail state
         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
@@ -2513,9 +2585,16 @@
         mCurrentPageScrollDiff = 0;
         mIgnoreResetTaskId = -1;
         mTaskListChangeId = -1;
-        mFocusedTaskViewId = -1;
+        setFocusedTaskViewId(INVALID_TASK_ID);
         mAnyTaskHasBeenDismissed = false;
 
+
+        if (enableRefactorTaskThumbnail()) {
+            // TODO(b/353917593): RecentsView is never destroyed, so its dependencies need to
+            //  be cleaned up during the reset, but re-created when RecentsView is "resumed".
+            // RecentsDependencies.Companion.destroy();
+        }
+
         Log.d(TAG, "reset - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile
                 + ", mRecentsAnimationController: " + mRecentsAnimationController);
         if (mEnableDrawingLiveTile) {
@@ -2537,14 +2616,19 @@
 
         // These are relatively expensive and don't need to be done this frame (RecentsView isn't
         // visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
-        post(() -> {
-            unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
-            setCurrentPage(0);
-            LayoutUtils.setViewEnabled(mActionsView, true);
-            if (mOrientationState.setGestureActive(false)) {
-                updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
-            }
-        });
+        post(this::onReset);
+    }
+
+    private void onReset() {
+        if (enableRefactorTaskThumbnail()) {
+            mRecentsViewModel.onReset();
+        }
+        unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+        setCurrentPage(0);
+        LayoutUtils.setViewEnabled(mActionsView, true);
+        if (mOrientationState.setGestureActive(false)) {
+            updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
+        }
     }
 
     public int getRunningTaskViewId() {
@@ -2579,8 +2663,7 @@
             return null;
         }
 
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             if (taskView.getTaskViewId() == taskViewId) {
                 return taskView;
             }
@@ -2626,6 +2709,7 @@
 
     /**
      * Get the index of the task view whose id matches {@param taskId}.
+     *
      * @return -1 if there is no task view for the task id, else the index of the task view.
      */
     public int getTaskIndexForId(int taskId) {
@@ -2640,9 +2724,12 @@
         if (!mModel.isTaskListValid(mTaskListChangeId)) {
             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
                     .getFilter(mFilterState.getPackageNameToFilter()));
+            Log.d(TAG, "reloadIfNeeded - getTasks: " + mTaskListChangeId);
             if (enableRefactorTaskThumbnail()) {
-                mTasksRepository.getAllTaskData(/* forceRefresh = */ true);
+                mRecentsViewModel.refreshAllTaskData();
             }
+        } else {
+            Log.d(TAG, "reloadIfNeeded - task list still valid: " + mTaskListChangeId);
         }
     }
 
@@ -2669,6 +2756,13 @@
         setTaskIconScaledDown(true);
     }
 
+    /**
+     * Returns whether the running task's attach alpha should be updated during the attach animation
+     */
+    public boolean shouldUpdateRunningTaskAlpha() {
+        return enableDesktopTaskAlphaAnimation() && getRunningTaskView() instanceof DesktopTaskView;
+    }
+
     private boolean isGestureActive() {
         return mActiveGestureRunningTasks != null;
     }
@@ -2718,8 +2812,8 @@
     }
 
     private void updateChildTaskOrientations() {
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            requireTaskViewAt(i).setOrientationState(mOrientationState);
+        for (TaskView taskView : getTaskViews()) {
+            taskView.setOrientationState(mOrientationState);
         }
         boolean shouldRotateMenuForFakeRotation =
                 !mOrientationState.isRecentsActivityRotationAllowed();
@@ -2782,6 +2876,14 @@
             animatorSet.play(
                     ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, splashAlpha));
         }
+        if (enableLargeDesktopWindowingTile()) {
+            if (animatorSet != null) {
+                animatorSet.play(
+                        ObjectAnimator.ofFloat(this, DESKTOP_CAROUSEL_DETACH_PROGRESS, 0f));
+            } else {
+                DESKTOP_CAROUSEL_DETACH_PROGRESS.set(this, 0f);
+            }
+        }
     }
 
     /**
@@ -2810,7 +2912,7 @@
         int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray();
         TaskView matchingTaskView = null;
         if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) {
-            // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single
+            // TODO(b/342635213): Unsure if it's expected, desktop runningTasks only have a single
             // taskId, therefore we match any DesktopTaskView that contains the runningTaskId.
             TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]);
             if (taskview instanceof DesktopTaskView) {
@@ -2833,6 +2935,7 @@
         if (runningTasks.length == 0) {
             return;
         }
+
         int runningTaskViewId = -1;
         boolean needGroupTaskView = runningTasks.length > 1;
         boolean needDesktopTask = hasDesktopTask(runningTasks);
@@ -2851,7 +2954,7 @@
                 // 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(mTmpRunningTasks[0], mTmpRunningTasks[1],
                         mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
             } else {
                 taskView = getTaskViewFromPool(TaskViewType.SINGLE);
@@ -2877,7 +2980,20 @@
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
         setCurrentTask(runningTaskViewId);
-        mFocusedTaskViewId = enableGridOnlyOverview() ? INVALID_TASK_ID : runningTaskViewId;
+
+        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);
@@ -2919,21 +3035,20 @@
     }
 
     private void setRunningTaskViewId(int runningTaskViewId) {
-        int prevRunningTaskViewId = mRunningTaskViewId;
         mRunningTaskViewId = runningTaskViewId;
 
         if (enableRefactorTaskThumbnail()) {
-            TaskView previousRunningTaskView = getTaskViewFromTaskViewId(prevRunningTaskViewId);
-            if (previousRunningTaskView != null) {
-                previousRunningTaskView.notifyIsRunningTaskUpdated();
-            }
-            TaskView newRunningTaskView = getTaskViewFromTaskViewId(runningTaskViewId);
-            if (newRunningTaskView != null) {
-                newRunningTaskView.notifyIsRunningTaskUpdated();
-            }
+            TaskView runningTaskView = getTaskViewFromTaskViewId(runningTaskViewId);
+            mRecentsViewModel.updateRunningTask(
+                    runningTaskView != null ? runningTaskView.getTaskIdSet()
+                            : Collections.emptySet());
         }
     }
 
+    private void setFocusedTaskViewId(int viewId) {
+        mFocusedTaskViewId = viewId;
+    }
+
     private int getTaskViewIdFromTaskId(int taskId) {
         TaskView taskView = getTaskViewByTaskId(taskId);
         return taskView != null ? taskView.getTaskViewId() : -1;
@@ -2944,30 +3059,48 @@
      */
     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) {
-            runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
-            if (!isHidden) {
-                AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
-                        AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
-            }
+        if (runningTask == null) {
+            return;
+        }
+        applyAttachAlpha();
+        if (!isHidden) {
+            AccessibilityManagerCompat.sendCustomAccessibilityEvent(
+                    runningTask, AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
         }
     }
 
+    private void applyAttachAlpha() {
+        // 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) {
+        setRunningTaskViewShowScreenshot(showScreenshot, /*updatedThumbnails=*/null);
+    }
+
+    private void setRunningTaskViewShowScreenshot(boolean showScreenshot,
+            @Nullable Map<Integer, ThumbnailData> updatedThumbnails) {
         mRunningTaskShowScreenshot = showScreenshot;
         TaskView runningTaskView = getRunningTaskView();
         if (runningTaskView != null) {
-            runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot);
+            runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot, updatedThumbnails);
+        }
+        if (enableRefactorTaskThumbnail()) {
+            mRecentsViewModel.setRunningTaskShowScreenshot(showScreenshot);
         }
     }
 
     public void setTaskIconScaledDown(boolean isScaledDown) {
         if (mTaskIconScaledDown != isScaledDown) {
             mTaskIconScaledDown = isScaledDown;
-            int taskCount = getTaskViewCount();
-            for (int i = 0; i < taskCount; i++) {
-                requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+            for (TaskView taskView : getTaskViews()) {
+                taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
             }
         }
     }
@@ -2980,9 +3113,7 @@
 
     public void animateUpTaskIconScale() {
         mTaskIconScaledDown = false;
-        int taskCount = getTaskViewCount();
-        for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             taskView.animateIconScaleAndDimIntoView();
         }
     }
@@ -3029,9 +3160,12 @@
         // 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 focusedTaskWidthAndSpacing = 0;
+        int largeTaskWidthAndSpacing = 0;
         int snappedTaskRowWidth = 0;
         int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
         TaskView snappedTaskView = getTaskViewAt(snappedPage);
@@ -3048,12 +3182,15 @@
             // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
             // which case keep tasks in their respective rows. For the running task, don't join
             // the grid.
-            if (taskView.isFocusedTask()) {
-                topRowWidth += taskWidthAndSpacing;
-                bottomRowWidth += taskWidthAndSpacing;
+            boolean isLargeTile = taskView.isLargeTile();
 
-                focusedTaskIndex = i;
-                focusedTaskWidthAndSpacing = taskWidthAndSpacing;
+            if (isLargeTile) {
+                // 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;
 
@@ -3061,15 +3198,18 @@
                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
                         - taskView.getLayoutParams().height) / 2f);
 
+                largeTasksIndices.add(i);
+                largeTaskWidthAndSpacing = taskWidthAndSpacing;
+
                 if (taskView == snappedTaskView) {
                     // If focused task is snapped, the row width is just task width and spacing.
                     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 ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+                            mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
                 } else {
                     // For task before the focused task, accumulate the width and spacing to
                     // calculate the distance focused task need to shift.
@@ -3105,7 +3245,7 @@
                     // Move horizontally into empty space.
                     float widthOffset = 0;
                     for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
-                        if (j == focusedTaskIndex) {
+                        if (largeTasksIndices.contains(j)) {
                             continue;
                         }
                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
@@ -3124,7 +3264,7 @@
                     // Move horizontally into empty space.
                     float widthOffset = 0;
                     for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
-                        if (j == focusedTaskIndex) {
+                        if (largeTasksIndices.contains(j)) {
                             continue;
                         }
                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
@@ -3174,12 +3314,22 @@
         // accordingly. Update longRowWidth if ClearAllButton has been moved.
         float clearAllShortTotalWidthTranslation = 0;
         int longRowWidth = Math.max(topRowWidth, bottomRowWidth);
-        if (longRowWidth < mLastComputedGridSize.width()) {
-            mClearAllShortTotalWidthTranslation =
-                    (mIsRtl
-                            ? mLastComputedTaskSize.right
-                            : deviceProfile.widthPx - mLastComputedTaskSize.left)
-                    - longRowWidth - deviceProfile.overviewGridSideMargin;
+
+        // If Recents contains only large task sizes, it should only consider 1 large size
+        // for ClearAllButton translation. The space at the left side of the large task will be
+        // empty and it should be move ClearAllButton further away as well.
+        // TODO(b/359573248): Validate the translation for ClearAllButton for grid only.
+        boolean hasOnlyLargeTasks = taskCount == largeTasksIndices.size();
+        if (enableLargeDesktopWindowingTile() && hasOnlyLargeTasks) {
+            longRowWidth = largeTaskWidthAndSpacing;
+        }
+
+        // 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) {
+            mClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
             clearAllShortTotalWidthTranslation = mIsRtl
                     ? -mClearAllShortTotalWidthTranslation : mClearAllShortTotalWidthTranslation;
             if (snappedTaskRowWidth == longRowWidth) {
@@ -3194,10 +3344,10 @@
         float clearAllTotalTranslationX =
                 clearAllAccumulatedTranslation + clearAllShorterRowCompensation
                         + clearAllShortTotalWidthTranslation + snappedTaskNonGridScrollAdjustment;
-        if (focusedTaskIndex < taskCount) {
+        if (!largeTasksIndices.isEmpty()) {
             // Shift by focused task's width and spacing if a task is focused.
             clearAllTotalTranslationX +=
-                    mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+                    mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
         }
 
         // Make sure there are enough space between snapped page and ClearAllButton, for the case
@@ -3209,9 +3359,9 @@
                     (mIsRtl
                             ? mLastComputedTaskSize.left
                             : deviceProfile.widthPx - mLastComputedTaskSize.right)
-                    - deviceProfile.overviewGridSideMargin - mPageSpacing
-                    + (mTaskWidth - snappedTaskView.getLayoutParams().width)
-                    - mClearAllShortTotalWidthTranslation;
+                            - deviceProfile.overviewGridSideMargin - mPageSpacing
+                            + (mTaskWidth - snappedTaskView.getLayoutParams().width)
+                            - mClearAllShortTotalWidthTranslation;
             if (distanceFromClearAll < minimumDistance) {
                 int distanceDifference = minimumDistance - distanceFromClearAll;
                 snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference;
@@ -3237,7 +3387,6 @@
         mClearAllButton.setGridScrollOffset(
                 mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left
                         : mLastComputedTaskSize.right - mLastComputedGridSize.right);
-
         setGridProgress(mGridProgress);
     }
 
@@ -3245,11 +3394,11 @@
         if (taskView1 == null || taskView2 == null) {
             return false;
         }
-        int taskViewId1 = taskView1.getTaskViewId();
-        int taskViewId2 = taskView2.getTaskViewId();
-        if (taskViewId1 == mFocusedTaskViewId || taskViewId2 == mFocusedTaskViewId) {
+        if (taskView1.isLargeTile() || taskView2.isLargeTile()) {
             return false;
         }
+        int taskViewId1 = taskView1.getTaskViewId();
+        int taskViewId2 = taskView2.getTaskViewId();
         return (mTopRowIdSet.contains(taskViewId1) && mTopRowIdSet.contains(taskViewId2)) || (
                 !mTopRowIdSet.contains(taskViewId1) && !mTopRowIdSet.contains(taskViewId2));
     }
@@ -3262,22 +3411,21 @@
     private void setGridProgress(float gridProgress) {
         mGridProgress = gridProgress;
 
-        int taskCount = getTaskViewCount();
-        for (int i = 0; i < taskCount; i++) {
-            requireTaskViewAt(i).setGridProgress(gridProgress);
+        for (TaskView taskView : getTaskViews()) {
+            taskView.setGridProgress(gridProgress);
         }
         mClearAllButton.setGridProgress(gridProgress);
     }
 
     private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
-        int taskCount = getTaskViewCount();
-        if (taskCount == 0) {
+        if (enableRefactorTaskThumbnail()) {
+            mRecentsViewModel.updateThumbnailSplashProgress(taskThumbnailSplashAlpha);
             return;
         }
 
         mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha;
-        for (int i = 0; i < taskCount; i++) {
-            requireTaskViewAt(i).setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
+        for (TaskView taskView : getTaskViews()) {
+            taskView.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
         }
     }
 
@@ -3291,12 +3439,12 @@
             mLayoutTransition.addTransitionListener(new TransitionListener() {
                 @Override
                 public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
+                        View view, int i) {
                 }
 
                 @Override
                 public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
+                        View view, int i) {
                     // When the unpinned task is added, snap to first page and disable transitions
                     if (view instanceof TaskView) {
                         snapToPage(0);
@@ -3399,6 +3547,7 @@
                 mSplitSelectStateController.getSplitAnimationController().
                         playAnimPlaceholderToFullscreen(mContainer, view,
                                 Optional.of(() -> resetFromSplitSelectionState())));
+        firstFloatingTaskView.setContentDescription(splitAnimInitProps.getContentDescription());
 
         // SplitInstructionsView: animate in
         safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
@@ -3448,15 +3597,18 @@
 
     /**
      * Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}.
-     * @param dismissedTaskView the {@link TaskView} to be dismissed
-     * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated
-     * @param shouldRemoveTask whether the associated {@link Task} should be removed from
-     *                         ActivityManager after dismissal
-     * @param duration duration of the animation
+     *
+     * @param dismissedTaskView           the {@link TaskView} to be dismissed
+     * @param animateTaskView             whether the {@link TaskView} to be dismissed should be
+     *                                    animated
+     * @param shouldRemoveTask            whether the associated {@link Task} should be removed from
+     *                                    ActivityManager after dismissal
+     * @param duration                    duration of the animation
      * @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) {
@@ -3471,7 +3623,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;
@@ -3481,25 +3634,32 @@
         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;
                 } else {
                     nextFocusedTaskFromTop =
-                            mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
+                            !mTopRowIdSet.isEmpty() && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
                     // Pick the next focused task from the preferred row.
-                    for (int i = 0; i < taskCount; i++) {
-                        TaskView taskView = requireTaskViewAt(i);
-                        if (taskView == dismissedTaskView) {
+                    for (TaskView taskView : getTaskViews()) {
+                        if (taskView == dismissedTaskView || taskView.isLargeTile()) {
                             continue;
                         }
                         boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
@@ -3515,13 +3675,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;
@@ -3534,31 +3694,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)) {
@@ -3570,17 +3742,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;
                 }
@@ -3626,8 +3787,7 @@
                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
                                 + (taskCount - 1) * halfAdditionalDismissTranslationOffset,
                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
-                for (int i = 0; i < taskCount; i++) {
-                    TaskView taskView = requireTaskViewAt(i);
+                for (TaskView taskView : getTaskViews()) {
                     anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff,
                             clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1));
                     dismissTranslationInterpolationEnd = Utilities.boundToRange(
@@ -3663,114 +3823,51 @@
         SplitAnimationTimings splitTimings =
                 AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().isTablet);
 
-        int distanceFromDismissedTask = 0;
+        int distanceFromDismissedTask = 1;
+        int stagingTranslation = 0;
+        if (isStagingFocusedTask || areAllDesktopTasksDismissed) {
+            int nextSnappedPage = isStagingFocusedTask
+                    ? indexOfChild(mUtils.getFirstSmallTaskView(getTaskViews()))
+                    : mUtils.getDesktopTaskViewCount(getTaskViews());
+            stagingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
+                    - getScrollForPage(nextSnappedPage);
+            stagingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
+                    : -newClearAllShortTotalWidthTranslation;
+        }
         for (int i = 0; i < count; i++) {
             View child = getChildAt(i);
             if (child == dismissedTaskView) {
-                if (animateTaskView) {
-                    if (dismissingForSplitSelection) {
-                        createInitialSplitSelectAnimation(anim);
-                    } else {
-                        addDismissedTaskAnimations(dismissedTaskView, duration, anim);
-                    }
+                if (animateTaskView && !dismissingForSplitSelection) {
+                    addDismissedTaskAnimations(dismissedTaskView, duration, anim);
                 }
-            } else if (!showAsGrid) {
-                // Compute scroll offsets from task dismissal for animation.
-                // If we just take newScroll - oldScroll, everything to the right of dragged task
-                // translates to the left. We need to offset this in some cases:
-                // - In RTL, add page offset to all pages, since we want pages to move to the right
-                // Additionally, add a page offset if:
-                // - Current page is rightmost page (leftmost for RTL)
-                // - Dragging an adjacent page on the left side (right side for RTL)
-                int offset = mIsRtl ? scrollDiffPerPage : 0;
-                if (mCurrentPage == dismissedIndex) {
-                    int lastPage = taskCount - 1;
-                    if (mCurrentPage == lastPage) {
-                        offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
-                    }
-                } else {
-                    // Dismissing an adjacent page.
-                    int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
-                    if (dismissedIndex == negativeAdjacent) {
-                        offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
-                    }
-                }
-
+            } else if (!showAsGrid || (enableLargeDesktopWindowingTile()
+                    && dismissedTaskView != null && dismissedTaskView.isLargeTile()
+                    && nextFocusedTaskView == null && !dismissingForSplitSelection)) {
+                int offset = getOffsetToDismissedTask(scrollDiffPerPage, dismissedIndex, taskCount);
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
-                    FloatProperty translationProperty = child instanceof TaskView
-                            ? ((TaskView) child).getPrimaryDismissTranslationProperty()
-                            : getPagedOrientationHandler().getPrimaryViewTranslate();
-
-                    float additionalDismissDuration =
-                            ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
-                                    i - dismissedIndex);
-
-                    // We are in non-grid layout.
-                    // If dismissing for split select, use split timings.
-                    // If not, use dismiss timings.
-                    float animationStartProgress = isSplitSelectionActive()
-                            ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f)
-                            : Utilities.boundToRange(
-                                    INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
-                                            + additionalDismissDuration, 0f, 1f);
-
-                    float animationEndProgress = isSplitSelectionActive()
-                            ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
-                                            + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
-                            : 1f;
-
-                    // Slide tiles in horizontally to fill dismissed area
-                    anim.setFloat(child, translationProperty, scrollDiff,
-                            clampToProgress(
-                                    splitTimings.getGridSlidePrimaryInterpolator(),
-                                    animationStartProgress,
-                                    animationEndProgress
-                            )
-                    );
-
-                    if (mEnableDrawingLiveTile && child instanceof TaskView
-                            && ((TaskView) child).isRunningTask()) {
-                        anim.addOnFrameCallback(() -> {
-                            runActionOnRemoteHandles(
-                                    remoteTargetHandle ->
-                                            remoteTargetHandle.getTaskViewSimulator()
-                                                    .taskPrimaryTranslation.value =
-                                                    getPagedOrientationHandler().getPrimaryValue(
-                                                            child.getTranslationX(),
-                                                            child.getTranslationY()
-                                                    ));
-                            redrawLiveTile();
-                        });
-                    }
+                    translateTaskWhenDismissed(
+                            child,
+                            Math.abs(i - dismissedIndex),
+                            scrollDiff,
+                            anim,
+                            splitTimings);
                     needsCurveUpdates = true;
                 }
-            } else if (child instanceof TaskView) {
-                TaskView taskView = (TaskView) child;
-                if (isFocusedTaskDismissed) {
-                    if (nextFocusedTaskView != null &&
-                            !isSameGridRow(taskView, nextFocusedTaskView)) {
-                        continue;
-                    }
-                } else {
-                    if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
-                        continue;
-                    }
-                }
+            } else if (child instanceof TaskView taskView) {
                 // Animate task with index >= dismissed index and in the same row as the
                 // dismissed index or next focused index. Offset successive task dismissal
                 // durations for a staggered effect.
-                distanceFromDismissedTask++;
-                int staggerColumn =  isStagingFocusedTask
+                int staggerColumn = isStagingFocusedTask
                         ? (int) Math.ceil(distanceFromDismissedTask / 2f)
                         : distanceFromDismissedTask;
                 // Set timings based on if user is initiating splitscreen on the focused task,
                 // or splitting/dismissing some other task.
                 float animationStartProgress = isStagingFocusedTask
                         ? Utilities.boundToRange(
-                                splitTimings.getGridSlideStartOffset()
-                                        + (splitTimings.getGridSlideStaggerOffset()
-                                        * staggerColumn),
+                        splitTimings.getGridSlideStartOffset()
+                                + (splitTimings.getGridSlideStaggerOffset()
+                                * staggerColumn),
                         0f,
                         dismissTranslationInterpolationEnd)
                         : Utilities.boundToRange(
@@ -3779,24 +3876,23 @@
                                         * staggerColumn, 0f, dismissTranslationInterpolationEnd);
                 float animationEndProgress = isStagingFocusedTask
                         ? Utilities.boundToRange(
-                                splitTimings.getGridSlideStartOffset()
-                                        + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
-                                        + splitTimings.getGridSlideDurationOffset(),
+                        splitTimings.getGridSlideStartOffset()
+                                + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
+                                + splitTimings.getGridSlideDurationOffset(),
                         0f,
                         dismissTranslationInterpolationEnd)
                         : 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;
@@ -3806,25 +3902,27 @@
                                     dismissTranslationInterpolationEnd));
                     anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
                             clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
-                } else {
-                    float primaryTranslation =
+                } else if ((isFocusedTaskDismissed && nextFocusedTaskView != null && isSameGridRow(
+                        taskView, nextFocusedTaskView))
+                        || (!isFocusedTaskDismissed && i >= dismissedIndex && isSameGridRow(
+                        taskView, dismissedTaskView))) {
+                    primaryTranslation +=
                             nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth;
-                    if (isStagingFocusedTask) {
-                        // Moves less if focused task is not in scroll position.
-                        int focusedTaskScroll = getScrollForPage(dismissedIndex);
-                        int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
-                        int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll;
-                        primaryTranslation +=
-                                mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff;
-                    }
+                }
+                primaryTranslation += mIsRtl ? stagingTranslation : -stagingTranslation;
 
+                if (primaryTranslation != 0) {
                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
                             mIsRtl ? primaryTranslation : -primaryTranslation,
                             clampToProgress(dismissInterpolator, animationStartProgress,
                                     animationEndProgress));
+                    distanceFromDismissedTask++;
                 }
             }
         }
+        if (dismissingForSplitSelection) {
+            createInitialSplitSelectAnimation(anim);
+        }
 
         if (needsCurveUpdates) {
             anim.addOnFrameCallback(this::updateCurveProperties);
@@ -3833,7 +3931,7 @@
         // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant
         // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we
         // want the dragged task to stay above all other views.
-        if (animateTaskView) {
+        if (animateTaskView && dismissedTaskView != null) {
             dismissedTaskView.setTranslationZ(0.1f);
         }
 
@@ -3842,12 +3940,13 @@
         final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll;
         final boolean finalSnapToLastTask = snapToLastTask;
         final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed;
-        mPendingAnimation.addEndListener(new Consumer<Boolean>() {
+        mPendingAnimation.addEndListener(new Consumer<>() {
             @Override
             public void accept(Boolean success) {
-                if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) {
+                if (mEnableDrawingLiveTile && dismissedTaskView != null
+                        && dismissedTaskView.isRunningTask() && success) {
                     finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
-                            () -> onEnd(success));
+                            () -> onEnd(true));
                 } else {
                     onEnd(success);
                 }
@@ -3861,12 +3960,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));
@@ -3963,9 +4062,9 @@
                     } else {
                         // Update focus task and its size.
                         if (finalIsFocusedTaskDismissed && finalNextFocusedTaskView != null) {
-                            mFocusedTaskViewId = enableGridOnlyOverview()
+                            setFocusedTaskViewId(enableGridOnlyOverview()
                                     ? INVALID_TASK_ID
-                                    : finalNextFocusedTaskView.getTaskViewId();
+                                    : finalNextFocusedTaskView.getTaskViewId());
                             mTopRowIdSet.remove(mFocusedTaskViewId);
                             finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
                         }
@@ -3997,8 +4096,9 @@
                                             RecentsView.this);
                                     int taskSize = (int) (
                                             getPagedOrientationHandler().getMeasuredSize(
-                                            taskView) * taskView
-                                            .getSizeAdjustment(/*fullscreenEnabled=*/false));
+                                                    taskView) * taskView
+                                                    .getSizeAdjustment(/*fullscreenEnabled=*/
+                                                            false));
                                     int taskEnd = taskStart + taskSize;
 
                                     shouldRebalance = taskEnd >= screenEnd - mPageSpacing;
@@ -4016,6 +4116,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.
@@ -4049,6 +4157,90 @@
     }
 
     /**
+     * Compute scroll offsets from task dismissal for animation.
+     * If we just take newScroll - oldScroll, everything to the right of dragged task
+     * translates to the left. We need to offset this in some cases:
+     * - In RTL, add page offset to all pages, since we want pages to move to the right
+     * Additionally, add a page offset if:
+     * - Current page is rightmost page (leftmost for RTL)
+     * - Dragging an adjacent page on the left side (right side for RTL)
+     */
+    private int getOffsetToDismissedTask(int scrollDiffPerPage, int dismissedIndex, int taskCount) {
+        // When mCurrentPage is ClearAllButton, use the last TaskView instead to calculate
+        // offset.
+        int currentPage = mCurrentPage == taskCount ? taskCount - 1 : mCurrentPage;
+        int offset = mIsRtl ? scrollDiffPerPage : 0;
+        if (currentPage == dismissedIndex) {
+            int lastPage = taskCount - 1;
+            if (currentPage == lastPage) {
+                offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+            }
+        } else {
+            // Dismissing an adjacent page.
+            int negativeAdjacent = currentPage - 1; // (Right in RTL, left in LTR)
+            if (dismissedIndex == negativeAdjacent) {
+                offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+            }
+        }
+        return offset;
+    }
+
+    private void translateTaskWhenDismissed(
+            View view,
+            int indexDiff,
+            int scrollDiffPerPage,
+            PendingAnimation pendingAnimation,
+            SplitAnimationTimings splitTimings) {
+        FloatProperty translationProperty = view instanceof TaskView
+                ? ((TaskView) view).getPrimaryDismissTranslationProperty()
+                : getPagedOrientationHandler().getPrimaryViewTranslate();
+
+        float additionalDismissDuration =
+                ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * indexDiff;
+
+        // We are in non-grid layout.
+        // If dismissing for split select, use split timings.
+        // If not, use dismiss timings.
+        float animationStartProgress = isSplitSelectionActive()
+                ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f)
+                : Utilities.boundToRange(
+                        INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+                                + additionalDismissDuration, 0f, 1f);
+
+        float animationEndProgress = isSplitSelectionActive()
+                ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
+                + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
+                : 1f;
+
+        // Slide tiles in horizontally to fill dismissed area
+        pendingAnimation.setFloat(
+                view,
+                translationProperty,
+                scrollDiffPerPage,
+                clampToProgress(
+                        splitTimings.getGridSlidePrimaryInterpolator(),
+                        animationStartProgress,
+                        animationEndProgress
+                )
+        );
+
+        if (mEnableDrawingLiveTile && view instanceof TaskView
+                && ((TaskView) view).isRunningTask()) {
+            pendingAnimation.addOnFrameCallback(() -> {
+                runActionOnRemoteHandles(
+                        remoteTargetHandle ->
+                                remoteTargetHandle.getTaskViewSimulator()
+                                        .taskPrimaryTranslation.value =
+                                        getPagedOrientationHandler().getPrimaryValue(
+                                                view.getTranslationX(),
+                                                view.getTranslationY()
+                                        ));
+                redrawLiveTile();
+            });
+        }
+    }
+
+    /**
      * Hides all overview actions if user is halfway through split selection, shows otherwise.
      * We only show split option if:
      * * Focused view is a single app
@@ -4086,9 +4278,8 @@
             return new IntArray(0);
         }
         IntArray topArray = new IntArray(mTopRowIdSet.size());
-        int taskViewCount = getTaskViewCount();
-        for (int i = 0; i < taskViewCount; i++) {
-            int taskViewId = requireTaskViewAt(i).getTaskViewId();
+        for (TaskView taskView : getTaskViews()) {
+            int taskViewId = taskView.getTaskViewId();
             if (mTopRowIdSet.contains(taskViewId)) {
                 topArray.add(taskViewId);
             }
@@ -4105,10 +4296,9 @@
             return new IntArray(0);
         }
         IntArray bottomArray = new IntArray(bottomRowIdArraySize);
-        int taskViewCount = getTaskViewCount();
-        for (int i = 0; i < taskViewCount; i++) {
-            int taskViewId = requireTaskViewAt(i).getTaskViewId();
-            if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
+        for (TaskView taskView : getTaskViews()) {
+            int taskViewId = taskView.getTaskViewId();
+            if (!mTopRowIdSet.contains(taskViewId) && !taskView.isLargeTile()) {
                 bottomArray.add(taskViewId);
             }
         }
@@ -4143,17 +4333,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(),
@@ -4166,9 +4363,8 @@
         }
         PendingAnimation anim = new PendingAnimation(duration);
 
-        int count = getTaskViewCount();
-        for (int i = 0; i < count; i++) {
-            addDismissedTaskAnimations(requireTaskViewAt(i), duration, anim);
+        for (TaskView taskView : getTaskViews()) {
+            addDismissedTaskAnimations(taskView, duration, anim);
         }
 
         mPendingAnimation = anim;
@@ -4215,7 +4411,7 @@
 
         // Init task grid nav helper with top/bottom id arrays.
         TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
-                getBottomRowIdArray(), mFocusedTaskViewId);
+                getBottomRowIdArray(), mUtils.getLargeTaskViewIds(getTaskViews()));
 
         // Get current page's task view ID.
         TaskView currentPageTaskView = getCurrentPageTaskView();
@@ -4246,8 +4442,10 @@
     private void dismissTask(int taskId) {
         TaskView taskView = getTaskViewByTaskId(taskId);
         if (taskView == null) {
+            Log.d(TAG, "dismissTask: " + taskId + ",  no associated TaskView");
             return;
         }
+        Log.d(TAG, "dismissTask: " + taskId);
         dismissTask(taskView, true /* animate */, false /* removeTask */);
     }
 
@@ -4331,13 +4529,8 @@
         alpha = Utilities.boundToRange(alpha, 0, 1);
         mContentAlpha = alpha;
 
-        TaskView runningTaskView = getRunningTaskView();
-        for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            TaskView child = requireTaskViewAt(i);
-            if (runningTaskView != null && mRunningTaskTileHidden && child == runningTaskView) {
-                continue;
-            }
-            child.setStableAlpha(alpha);
+        for (TaskView taskView : getTaskViews()) {
+            taskView.setStableAlpha(alpha);
         }
         mClearAllButton.setContentAlpha(mContentAlpha);
         int alphaInt = Math.round(alpha * 255);
@@ -4415,6 +4608,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());
     }
@@ -4446,6 +4653,13 @@
         return Objects.requireNonNull(getTaskViewAt(index));
     }
 
+    /**
+     * Returns the current list of [TaskView] children.
+     */
+    private Iterable<TaskView> getTaskViews() {
+        return mUtils.getTaskViews(getTaskViewCount(), this::requireTaskViewAt);
+    }
+
     public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
         mOnEmptyMessageUpdatedListener = listener;
     }
@@ -4542,9 +4756,14 @@
                 ? (runningTask == null ? INVALID_PAGE : indexOfChild(runningTask))
                 : mOffsetMidpointIndexOverride;
         int modalMidpoint = getCurrentPage();
-        boolean isModalGridWithoutFocusedTask =
-                showAsGrid && enableGridOnlyOverview() && mTaskModalness > 0;
-        if (isModalGridWithoutFocusedTask) {
+        TaskView carouselHiddenMidpointTask = runningTask != null ? runningTask
+                : mUtils.getFirstTaskViewInCarousel(/*nonRunningTaskCarouselHidden=*/true,
+                        getTaskViews(), null);
+        int carouselHiddenMidpoint = indexOfChild(carouselHiddenMidpointTask);
+        boolean shouldCalculateOffsetForAllTasks = showAsGrid
+                && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile())
+                && mTaskModalness > 0;
+        if (shouldCalculateOffsetForAllTasks) {
             modalMidpoint = indexOfChild(mSelectedTask);
         }
 
@@ -4560,6 +4779,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
@@ -4577,23 +4797,44 @@
                     : 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
                             ? leftOffsetSize
                             : rightOffsetSize;
-            if (isModalGridWithoutFocusedTask) {
+            if (shouldCalculateOffsetForAllTasks) {
                 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;
-            float totalTranslationX = translation + modalTranslation;
-            View child = getChildAt(i);
+            boolean skipTranslationOffset = enableDesktopTaskAlphaAnimation()
+                    && i == getRunningTaskIndex()
+                    && child instanceof DesktopTaskView;
+            float totalTranslationX = (skipTranslationOffset ? 0f : translation) + modalTranslation
+                    + carouselHiddenOffsetSize;
             FloatProperty translationPropertyX = child instanceof TaskView
                     ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
                     : getPagedOrientationHandler().getPrimaryViewTranslate();
@@ -4641,6 +4882,7 @@
     /**
      * Computes the distance to offset the given child such that it is completely offscreen when
      * translating away from the given midpoint.
+     *
      * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
      */
     private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
@@ -4649,6 +4891,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;
@@ -4708,7 +4958,7 @@
             }
             distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
         }
-        return distanceToOffscreen * offsetProgress;
+        return distanceToOffscreen;
     }
 
     /**
@@ -4745,9 +4995,9 @@
 
     protected void setTaskViewsResistanceTranslation(float translation) {
         mTaskViewsSecondaryTranslation = translation;
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView task = requireTaskViewAt(i);
-            task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
+        for (TaskView taskView : getTaskViews()) {
+            taskView.getTaskResistanceTranslationProperty().set(taskView,
+                    translation / getScaleY());
         }
         runActionOnRemoteHandles(
                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
@@ -4755,23 +5005,21 @@
     }
 
     private void updateTaskViewsSnapshotRadius() {
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            requireTaskViewAt(i).updateSnapshotRadius();
+        for (TaskView taskView : getTaskViews()) {
+            taskView.updateSnapshotRadius();
         }
     }
 
     protected void setTaskViewsPrimarySplitTranslation(float translation) {
         mTaskViewsPrimarySplitTranslation = translation;
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView task = requireTaskViewAt(i);
-            task.getPrimarySplitTranslationProperty().set(task, translation);
+        for (TaskView taskView : getTaskViews()) {
+            taskView.getPrimarySplitTranslationProperty().set(taskView, translation);
         }
     }
 
     protected void setTaskViewsSecondarySplitTranslation(float translation) {
         mTaskViewsSecondarySplitTranslation = translation;
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) {
                 continue;
             }
@@ -4808,7 +5056,6 @@
         mSplitSelectStateController.setAnimateCurrentTaskDismissal(
                 true /*animateCurrentTaskDismissal*/);
         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
-        updateDesktopTaskVisibility(false /* visible */);
     }
 
     /**
@@ -4822,20 +5069,45 @@
         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
                 && mSplitHiddenTaskView instanceof GroupedTaskView);
         mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
-                splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
+                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);
+                }
+            }
         }
     }
 
@@ -4860,22 +5132,33 @@
             mSplitSelectStateController.getSplitAnimationController()
                     .addInitialSplitFromPair(taskContainer, builder,
                             mContainer.getDeviceProfile(),
-                            mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
+                            mSplitHiddenTaskView.getLayoutParams().width,
+                            mSplitHiddenTaskView.getLayoutParams().height,
                             primaryTaskSelected);
-            builder.addOnFrameCallback(() ->{
-                // TODO(b/334826842): Handle splash icon for new TTV.
+            builder.addOnFrameCallback(() -> {
                 if (!enableRefactorTaskThumbnail()) {
                     taskContainer.getThumbnailViewDeprecated().refreshSplashView();
                 }
                 mSplitHiddenTaskView.updateSnapshotRadius();
             });
         } else if (isInitiatingSplitFromTaskView) {
+            if (Flags.enableHoverOfChildElementsInTaskview()) {
+                mSplitHiddenTaskView.setBorderEnabled(false);
+            }
             // Splitting from Overview for fullscreen task
             createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration,
                     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);
+            }
         }
     }
 
@@ -4886,17 +5169,21 @@
      * @param containerTaskView If our second selected app is currently running in Recents, this is
      *                          the "container" TaskView from Recents. If we are starting a fresh
      *                          instance of the app from an Intent, this will be null.
-     * @param task The Task corresponding to our second selected app. If we are starting a fresh
-     *             instance of the app from an Intent, this will be null.
-     * @param drawable The Drawable corresponding to our second selected app's icon.
-     * @param secondView The View representing the current space on the screen where the second app
-     *                   is (either the ThumbnailView or the tapped icon).
-     * @param intent If we are launching a fresh instance of the app, this is the Intent for it. If
-     *               the second app is already running in Recents, this will be null.
-     * @param user If we are launching a fresh instance of the app, this is the UserHandle for it.
-     *             If the second app is already running in Recents, this will be null.
+     * @param task              The Task corresponding to our second selected app. If we are
+     *                          starting a fresh
+     *                          instance of the app from an Intent, this will be null.
+     * @param drawable          The Drawable corresponding to our second selected app's icon.
+     * @param secondView        The View representing the current space on the screen where the
+     *                          second app
+     *                          is (either the ThumbnailView or the tapped icon).
+     * @param intent            If we are launching a fresh instance of the app, this is the Intent
+     *                          for it. If
+     *                          the second app is already running in Recents, this will be null.
+     * @param user              If we are launching a fresh instance of the app, this is the
+     *                          UserHandle for it.
+     *                          If the second app is already running in Recents, this will be null.
      * @return true if waiting for confirmation of second app or if split animations are running,
-     *          false otherwise
+     * false otherwise
      */
     public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable,
             View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user,
@@ -4984,7 +5271,7 @@
                 "Second tile selected");
 
         // Fade out all other views underneath placeholders
-        ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0);
+        ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA, 1, 0);
         pendingAnimation.add(tvFade, DECELERATE_2, SpringProperty.DEFAULT);
         pendingAnimation.buildAnim().start();
         return true;
@@ -5037,9 +5324,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) {
@@ -5132,7 +5425,7 @@
         if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
             int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
             mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
-                    mEmptyMessagePaint, availableWidth)
+                            mEmptyMessagePaint, availableWidth)
                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
                     .build();
             int totalHeight = mEmptyTextLayout.getHeight()
@@ -5174,23 +5467,46 @@
      * to the right.
      */
     @SuppressLint("Recycle")
-    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
+    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView taskView) {
         AnimatorSet anim = new AnimatorSet();
 
-        int taskIndex = indexOfChild(tv);
+        int taskIndex = indexOfChild(taskView);
         int centerTaskIndex = getCurrentPage();
 
         float toScale = getMaxScaleForFullScreen();
         boolean showAsGrid = showAsGrid();
-        boolean launchingCenterTask = showAsGrid
-                ? tv.isFocusedTask() && isTaskViewFullyVisible(tv)
-                : taskIndex == centerTaskIndex;
-        if (launchingCenterTask) {
+        boolean zoomInTaskView = showAsGrid ? taskView.isLargeTile() : taskIndex == centerTaskIndex;
+        if (zoomInTaskView) {
             anim.play(ObjectAnimator.ofFloat(this, RECENTS_SCALE_PROPERTY, toScale));
             anim.play(ObjectAnimator.ofFloat(this, FULLSCREEN_PROGRESS, 1));
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(@NonNull Animator animation, boolean isReverse) {
+                    taskView.getThumbnailBounds(mTempRect, /*relativeToDragLayer=*/true);
+                    getTaskDimension(mContext, mContainer.getDeviceProfile(), mTempPointF);
+                    Rect fullscreenBounds = new Rect(0, 0, (int) mTempPointF.x,
+                            (int) mTempPointF.y);
+                    Utilities.getPivotsForScalingRectToRect(mTempRect, fullscreenBounds,
+                            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) {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
-            float displacementX = tv.getWidth() * (toScale - 1f);
+            float displacementX = taskView.getWidth() * (toScale - 1f);
             float primaryTranslation = mIsRtl ? -displacementX : displacementX;
             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
                     getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation));
@@ -5218,6 +5534,17 @@
             }
         }
         anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0, 1));
+        if (taskView instanceof DesktopTaskView) {
+            anim.play(ObjectAnimator.ofArgb(mContainer.getScrimView(), VIEW_BACKGROUND_COLOR,
+                    Color.TRANSPARENT));
+        }
+        DepthController depthController = getDepthController();
+        if (depthController != null) {
+            float targetDepth = taskView instanceof DesktopTaskView ? 0 : BACKGROUND_APP.getDepth(
+                    mContainer);
+            anim.play(ObjectAnimator.ofFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
+                    targetDepth));
+        }
         return anim;
     }
 
@@ -5243,7 +5570,7 @@
     }
 
     public PendingAnimation createTaskLaunchAnimation(
-            TaskView tv, long duration, Interpolator interpolator) {
+            TaskView taskView, long duration, Interpolator interpolator) {
         if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
             throw new IllegalStateException("Another pending animation is still running");
         }
@@ -5258,13 +5585,13 @@
         updateGridProperties();
         updateScrollSynchronously();
 
-        int targetSysUiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
-        final boolean[] passedOverviewThreshold = new boolean[] {false};
-        ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
-        progressAnim.addUpdateListener(animator -> {
+        int targetSysUiFlags = taskView.getTaskContainers().getFirst().getSysUiStatusNavFlags();
+        final boolean[] passedOverviewThreshold = new boolean[]{false};
+        AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(taskView);
+        anim.play(new AnimatedFloat(v -> {
             // Once we pass a certain threshold, update the sysui flags to match the target
             // tasks' flags
-            if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) {
+            if (v > UPDATE_SYSUI_FLAGS_THRESHOLD) {
                 mContainer.getSystemUiController().updateUiState(
                         UI_STATE_FULLSCREEN_TASK, targetSysUiFlags);
             } else {
@@ -5272,8 +5599,7 @@
             }
 
             // Passing the threshold from taskview to fullscreen app will vibrate
-            final boolean passed = animator.getAnimatedFraction() >=
-                    SUCCESS_TRANSITION_PROGRESS;
+            final boolean passed = v >= SUCCESS_TRANSITION_PROGRESS;
             if (passed != passedOverviewThreshold[0]) {
                 passedOverviewThreshold[0] = passed;
                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
@@ -5283,19 +5609,7 @@
                     mRecentsAnimationController.setWillFinishToHome(!passed);
                 }
             }
-        });
-
-        AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
-
-        DepthController depthController = getDepthController();
-        if (depthController != null) {
-            ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController.stateDepth,
-                    MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(mContainer));
-            anim.play(depthAnimator);
-        }
-        anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0f, 1f));
-
-        anim.play(progressAnim);
+        }).animateToValue(0f, 1f));
         anim.setInterpolator(interpolator);
 
         mPendingAnimation = new PendingAnimation(duration);
@@ -5304,9 +5618,16 @@
                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
                         .addOverviewToAppAnim(mPendingAnimation, interpolator));
         mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+        mPendingAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                runActionOnRemoteHandles(remoteTargetHandle ->
+                        remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false));
+            }
+        });
         mPendingAnimation.addEndListener(isSuccess -> {
             if (isSuccess) {
-                if (tv instanceof GroupedTaskView && hasAllValidTaskIds(tv.getTaskIds())
+                if (taskView instanceof GroupedTaskView && hasAllValidTaskIds(taskView.getTaskIds())
                         && mRemoteTargetHandles != null) {
                     // TODO(b/194414938): make this part of the animations instead.
                     TaskViewUtils.createSplitAuxiliarySurfacesAnimator(
@@ -5316,13 +5637,13 @@
                                 dividerAnimator.end();
                             });
                 }
-                if (tv.isRunningTask()) {
+                if (taskView.isRunningTask()) {
                     finishRecentsAnimation(false /* toRecents */, null);
                     onTaskLaunchAnimationEnd(true /* success */);
                 } else {
-                    tv.launchTask(this::onTaskLaunchAnimationEnd);
+                    taskView.launchWithoutAnimation(this::onTaskLaunchAnimationEnd);
                 }
-                mContainer.getStatsLogManager().logger().withItemInfo(tv.getFirstItemInfo())
+                mContainer.getStatsLogManager().logger().withItemInfo(taskView.getFirstItemInfo())
                         .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
             } else {
                 onTaskLaunchAnimationEnd(false);
@@ -5335,6 +5656,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;
     }
@@ -5380,7 +5708,7 @@
                     fullyVisibleTaskIds.addAll(taskIds);
                 }
             }
-            mRecentsViewData.getSettledFullyVisibleTaskIds().setValue(fullyVisibleTaskIds);
+            mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
         }
     }
 
@@ -5463,7 +5791,7 @@
         }
 
         RemoteTargetGluer gluer;
-        if (recentsAnimationTargets.hasDesktopTasks()) {
+        if (recentsAnimationTargets.hasDesktopTasks(mContext)) {
             gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
                     true /* forDesktop */);
             mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
@@ -5645,26 +5973,45 @@
     }
 
     private int getFirstViewIndex() {
-        TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null;
-        return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0;
+        final TaskView firstView;
+        if (mShowAsGridLastOnLayout) {
+            // 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(),
+                    isSplitSelectionActive());
+            if (firstLargeTaskView != null) {
+                firstView = firstLargeTaskView;
+            } else {
+                firstView = mUtils.getFirstSmallTaskView(getTaskViews());
+            }
+        } else {
+            firstView = mUtils.getFirstTaskViewInCarousel(
+                    /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0,
+                    getTaskViews(), getRunningTaskView());
+        }
+        return indexOfChild(firstView);
     }
 
     private int getLastViewIndex() {
+        final View lastView;
         if (!mDisallowScrollToClearAll) {
-            return indexOfChild(mClearAllButton);
+            // When ClearAllButton is present, it always end with ClearAllButton.
+            lastView = mClearAllButton;
+        } else if (mShowAsGridLastOnLayout) {
+            // When ClearAllButton is absent, for the grid Overview, it always end with a grid task
+            // if they exist, otherwise it ends with a large tile (focused task or desktop task).
+            TaskView lastGridTaskView = getLastGridTaskView();
+            if (lastGridTaskView != null) {
+                lastView = lastGridTaskView;
+            } else {
+                lastView = mUtils.getLastLargeTaskView(getTaskViews());
+            }
+        } else {
+            lastView = mUtils.getLastTaskViewInCarousel(
+                    /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0,
+                    getTaskViews(), getRunningTaskView());
         }
-
-        if (!mShowAsGridLastOnLayout) {
-            return getTaskViewCount() - 1;
-        }
-
-        TaskView lastGridTaskView = getLastGridTaskView();
-        if (lastGridTaskView != null) {
-            return indexOfChild(lastGridTaskView);
-        }
-
-        // Returns focus task if there are no grid tasks.
-        return indexOfChild(getFocusedTaskView());
+        return indexOfChild(lastView);
     }
 
     /**
@@ -5779,6 +6126,7 @@
      * Sets whether or not we should clamp the scroll offset.
      * This is used to avoid x-axis movement when swiping up transient taskbar.
      * Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
+     *
      * @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp threshold is
      *                          met.
      */
@@ -5811,11 +6159,11 @@
         // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
         // the page can move freely given there's no visual indication why it shouldn't.
         int overScrollShift = mAdjacentPageHorizontalOffset > 0
-                        ? (int) Utilities.mapRange(
-                                mAdjacentPageHorizontalOffset,
-                                getOverScrollShift(),
-                                getUndampedOverScrollShift())
-                        : getOverScrollShift();
+                ? (int) Utilities.mapRange(
+                mAdjacentPageHorizontalOffset,
+                getOverScrollShift(),
+                getUndampedOverScrollShift())
+                : getOverScrollShift();
         return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
                 + overScrollShift + getOffsetFromScrollPosition(pageIndex);
     }
@@ -5879,7 +6227,7 @@
     public boolean isOnGridBottomRow(TaskView taskView) {
         return showAsGrid()
                 && !mTopRowIdSet.contains(taskView.getTaskViewId())
-                && taskView.getTaskViewId() != mFocusedTaskViewId;
+                && !taskView.isLargeTile();
     }
 
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
@@ -5913,9 +6261,7 @@
 
     private void updateEnabledOverlays() {
         TaskView focusedTaskView = getFocusedTaskView();
-        int taskCount = getTaskViewCount();
-        for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = requireTaskViewAt(i);
+        for (TaskView taskView : getTaskViews()) {
             if (taskView == focusedTaskView) {
                 continue;
             }
@@ -5932,7 +6278,10 @@
         if (mOverlayEnabled != overlayEnabled) {
             mOverlayEnabled = overlayEnabled;
             updateEnabledOverlays();
-            mRecentsViewData.getOverlayEnabled().setValue(overlayEnabled);
+
+            if (enableRefactorTaskThumbnail()) {
+                mRecentsViewModel.setOverlayEnabled(overlayEnabled);
+            }
         }
     }
 
@@ -5980,38 +6329,20 @@
             return;
         }
 
-        switchToScreenshotInternal(onFinishRunnable);
-    }
-
-    private void switchToScreenshotInternal(Runnable onFinishRunnable) {
-        // TODO(b/342560598): Handle switchToScreenshot for new TTV.
-        if (enableRefactorTaskThumbnail()) {
-            onFinishRunnable.run();
-            return;
-        }
-
         TaskView taskView = getRunningTaskView();
         if (taskView == null) {
             onFinishRunnable.run();
             return;
         }
 
-        setRunningTaskViewShowScreenshot(true);
-        for (TaskContainer container : taskView.getTaskContainers()) {
-            if (container == null) {
-                continue;
-            }
-
-            ThumbnailData td =
-                    mRecentsAnimationController.screenshotTask(container.getTask().key.id);
-            TaskThumbnailViewDeprecated thumbnailView = container.getThumbnailViewDeprecated();
-            if (td != null) {
-                thumbnailView.setThumbnail(container.getTask(), td);
-            } else {
-                thumbnailView.refresh();
-            }
+        Map<Integer, ThumbnailData> updatedThumbnails = mUtils.screenshotTasks(taskView,
+                mRecentsAnimationController);
+        if (enableRefactorTaskThumbnail()) {
+            mHelper.switchToScreenshot(taskView, updatedThumbnails, onFinishRunnable);
+        } else {
+            setRunningTaskViewShowScreenshot(true, updatedThumbnails);
+            ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
         }
-        ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
     }
 
     /**
@@ -6025,9 +6356,12 @@
             Runnable onFinishRunnable) {
         final TaskView taskView = getRunningTaskView();
         if (taskView != null) {
-            taskView.setShouldShowScreenshot(true);
-            taskView.refreshThumbnails(thumbnailDatas);
-            ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
+            if (enableRefactorTaskThumbnail()) {
+                mHelper.switchToScreenshot(taskView, thumbnailDatas, onFinishRunnable);
+            } else {
+                taskView.setShouldShowScreenshot(true, thumbnailDatas);
+                ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
+            }
         } else {
             onFinishRunnable.run();
         }
@@ -6077,7 +6411,6 @@
      * tasks to be dimmed while other elements in the recents view are left alone.
      */
     public void showForegroundScrim(boolean show) {
-        // TODO(b/349601769) Add scrim response into new TTV - this is called from overlay
         if (!show && mColorTint == 0) {
             if (mTintingAnimator != null) {
                 mTintingAnimator.cancel();
@@ -6096,8 +6429,12 @@
     private void setColorTint(float tintAmount) {
         mColorTint = tintAmount;
 
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
+        if (enableRefactorTaskThumbnail()) {
+            mRecentsViewModel.setTintAmount(tintAmount);
+        }
+
+        for (TaskView taskView : getTaskViews()) {
+            taskView.setColorTint(mColorTint, mTintingColor);
         }
 
         Drawable scrimBg = mContainer.getScrimView().getBackground();
@@ -6120,7 +6457,7 @@
     public boolean showAsGrid() {
         return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
                 && mSizeStrategy.stateFromGestureEndTarget(mCurrentGestureEndTarget)
-                    .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+                .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
     }
 
     private boolean showAsFullscreen() {
@@ -6161,7 +6498,7 @@
 
     /**
      * @return Corner radius in pixel value for PiP window, which is updated via
-     *         {@link #mIPipAnimationListener}
+     * {@link #mIPipAnimationListener}
      */
     public int getPipCornerRadius() {
         return mPipCornerRadius;
@@ -6169,7 +6506,7 @@
 
     /**
      * @return Shadow radius in pixel value for PiP window, which is updated via
-     *         {@link #mIPipAnimationListener}
+     * {@link #mIPipAnimationListener}
      */
     public int getPipShadowRadius() {
         return mPipShadowRadius;
@@ -6387,6 +6724,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 060c71e..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,11 +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.util.SystemUiController;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
+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
@@ -41,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) {
@@ -52,11 +56,6 @@
     }
 
     /**
-     * Returns {@link SystemUiController} to manage various window flags to control system UI.
-     */
-    SystemUiController getSystemUiController();
-
-    /**
      * Returns {@link ScrimView}
      */
     ScrimView getScrimView();
@@ -92,7 +91,7 @@
     /**
      * Returns overview actions view as a view
      */
-    View getActionsView();
+    OverviewActionsView getActionsView();
 
     /**
      * @see BaseActivity#addForceInvisibleFlag(int)
@@ -140,12 +139,6 @@
     void runOnBindToTouchInteractionService(Runnable r);
 
     /**
-     * @see Activity#getWindow()
-     * @return Window
-     */
-    Window getWindow();
-
-    /**
      * @see
      * BaseActivity#addMultiWindowModeChangedListener(BaseActivity.MultiWindowModeChangedListener)
      * @param listener {@link BaseActivity.MultiWindowModeChangedListener}
@@ -174,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.
      *
@@ -198,4 +210,13 @@
                                         .setOrientationHandler(orientationForLogging))
                         .build());
     }
+
+    @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
new file mode 100644
index 0000000..3616fbb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.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.quickstep.views
+
+import com.android.quickstep.ViewUtils
+import com.android.quickstep.recents.viewmodel.RecentsViewModel
+import com.android.systemui.shared.recents.model.ThumbnailData
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+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) {
+    private lateinit var viewAttachedScope: CoroutineScope
+
+    fun onAttachedToWindow() {
+        viewAttachedScope =
+            CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("RecentsView"))
+    }
+
+    fun onDetachedFromWindow() {
+        viewAttachedScope.cancel("RecentsView detaching from window")
+    }
+
+    fun switchToScreenshot(
+        taskView: TaskView,
+        updatedThumbnails: Map<Int, ThumbnailData>?,
+        onFinishRunnable: Runnable,
+    ) {
+        // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside
+        // viewAttachedScope.
+        recentsViewModel.setRunningTaskShowScreenshot(true)
+        viewAttachedScope.launch {
+            recentsViewModel.waitForRunningTaskShowScreenshotToUpdate()
+            recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails)
+            withContext(Dispatchers.Main) { ViewUtils.postFrameDrawn(taskView, onFinishRunnable) }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 3d994e8..f6393e4 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,13 +16,11 @@
 
 package com.android.quickstep.views;
 
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
 import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -42,9 +40,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitSelectStateController;
 
 /**
@@ -57,7 +54,6 @@
 public class SplitInstructionsView extends LinearLayout {
     private static final int BOUNCE_DURATION = 250;
     private static final float BOUNCE_HEIGHT = 20;
-    private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
 
     private final RecentsViewContainer mContainer;
     public boolean mIsCurrentlyAnimating = false;
@@ -165,25 +161,11 @@
     private void exitSplitSelection() {
         RecentsView recentsView = mContainer.getOverviewPanel();
         SplitSelectStateController splitSelectController = recentsView.getSplitSelectController();
-
         StateManager stateManager = recentsView.getStateManager();
-        BaseState startState = stateManager.getState();
-        long duration = startState.getTransitionDuration(mContainer.asContext(), false);
-        if (duration == 0) {
-            // Case where we're in contextual on workspace (NORMAL), which by default has 0
-            // transition duration
-            duration = DURATION_DEFAULT_SPLIT_DISMISS;
-        }
-        StateAnimationConfig config = new StateAnimationConfig();
-        config.duration = duration;
-        AnimatorSet stateAnim = stateManager.createAtomicAnimation(
-                startState, NORMAL, config);
-        AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController()
-                .createPlaceholderDismissAnim(mContainer,
-                        LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration);
-        stateAnim.play(dismissAnim);
-        stateManager.setCurrentAnimation(stateAnim, NORMAL);
-        stateAnim.start();
+
+        AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+                LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON,
+                splitSelectController.getSplitAnimationController());
     }
 
     void ensureProperRotation() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 74d120f..c940fb4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -29,10 +29,15 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskUtils
-import com.android.quickstep.task.thumbnail.TaskThumbnail
+import com.android.quickstep.ViewUtils.addAccessibleChildToList
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
+import com.android.quickstep.recents.di.getScope
+import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.util.GetThumbnailUseCase
 import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.systemui.shared.recents.model.Task
 
 /** Holder for all Task dependent information. */
@@ -52,24 +57,34 @@
     @SplitConfigurationOptions.StagePosition val stagePosition: Int,
     val digitalWellBeingToast: DigitalWellBeingToast?,
     val showWindowsView: View?,
-    taskOverlayFactory: TaskOverlayFactory
+    taskOverlayFactory: TaskOverlayFactory,
 ) {
     val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
-    val taskContainerData = TaskContainerData()
+    lateinit var taskContainerData: TaskContainerData
 
-    private val getThumbnailUseCase by lazy {
-        // TODO(b/335649589): Ideally create and obtain this from DI.
-        val recentsView =
-            RecentsViewContainer.containerFromContext<RecentsViewContainer>(
-                    overlay.taskView.context
-                )
-                .getOverviewPanel<RecentsView<*, *>>()
-        GetThumbnailUseCase(recentsView.mTasksRepository!!)
+    private val taskThumbnailViewModel: TaskThumbnailViewModel by
+        RecentsDependencies.inject(snapshotView)
+
+    // TODO(b/335649589): Ideally create and obtain this from DI.
+    private val taskContainerViewModel: TaskContainerViewModel by lazy {
+        TaskContainerViewModel(
+            sysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+            getThumbnailUseCase = RecentsDependencies.get(),
+            splashAlphaUseCase = RecentsDependencies.get(),
+        )
     }
 
     init {
         if (enableRefactorTaskThumbnail()) {
             require(snapshotView is TaskThumbnailView)
+            taskContainerData = RecentsDependencies.get(this)
+            RecentsDependencies.getScope(snapshotView).apply {
+                val taskViewScope = RecentsDependencies.getScope(taskView)
+                linkTo(taskViewScope)
+
+                val taskContainerScope = RecentsDependencies.getScope(this@TaskContainer)
+                linkTo(taskContainerScope)
+            }
         } else {
             require(snapshotView is TaskThumbnailViewDeprecated)
         }
@@ -78,7 +93,7 @@
     val splitAnimationThumbnail: Bitmap?
         get() =
             if (enableRefactorTaskThumbnail()) {
-                getThumbnailUseCase.run(task.key.id)
+                taskContainerViewModel.getThumbnail(task.key.id)
             } else {
                 thumbnailViewDeprecated.thumbnail
             }
@@ -95,16 +110,17 @@
             return snapshotView as TaskThumbnailViewDeprecated
         }
 
-    // TODO(b/334826842): Support shouldShowSplashView for new TTV.
     val shouldShowSplashView: Boolean
         get() =
-            if (enableRefactorTaskThumbnail()) false
+            if (enableRefactorTaskThumbnail())
+                taskContainerViewModel.shouldShowThumbnailSplash(task.key.id)
             else thumbnailViewDeprecated.shouldShowSplashView()
 
-    // TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
     val sysUiStatusNavFlags: Int
         get() =
-            if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
+            if (enableRefactorTaskThumbnail())
+                taskContainerViewModel.getSysUiStatusNavFlags(task.key.id)
+            else thumbnailViewDeprecated.sysUiStatusNavFlags
 
     /** Builds proto for logging */
     val itemInfo: WorkspaceItemInfo
@@ -130,28 +146,28 @@
             }
 
     fun bind() {
+        digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
         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)
+        }
     }
 
-    // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
-    //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
     fun bindThumbnailView() {
-        // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
-        //  this should be decided inside TaskThumbnailViewModel.
-        thumbnailView.viewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
+        taskThumbnailViewModel.bind(task.key.id)
     }
 
     fun setOverlayEnabled(enabled: Boolean) {
@@ -159,4 +175,11 @@
             thumbnailViewDeprecated.setOverlayEnabled(enabled)
         }
     }
+
+    fun addChildForAccessibility(outChildren: ArrayList<View>) {
+        addAccessibleChildToList(iconView.asView(), outChildren)
+        addAccessibleChildToList(snapshotView, outChildren)
+        showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
+        digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index e10d38c..19d706f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -42,7 +42,11 @@
     companion object {
         const val TAG = "TaskMenuViewWithArrow"
 
-        fun showForTask(taskContainer: TaskContainer, alignedOptionIndex: Int = 0): Boolean {
+        fun showForTask(
+            taskContainer: TaskContainer,
+            alignedOptionIndex: Int = 0,
+            onClosedCallback: Runnable? = null
+        ): Boolean {
             val container: RecentsViewContainer =
                 RecentsViewContainer.containerFromContext(taskContainer.taskView.context)
             val taskMenuViewWithArrow =
@@ -52,7 +56,11 @@
                     false
                 ) as TaskMenuViewWithArrow<*>
 
-            return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex)
+            return taskMenuViewWithArrow.populateAndShowForTask(
+                taskContainer,
+                alignedOptionIndex,
+                onClosedCallback
+            )
         }
     }
 
@@ -91,13 +99,14 @@
     private var optionMeasuredHeight = 0
     private val arrowHorizontalPadding: Int
         get() =
-            if (taskView.isFocusedTask)
+            if (taskView.isLargeTile)
                 resources.getDimensionPixelSize(R.dimen.task_menu_horizontal_padding)
             else 0
 
     private var iconView: IconView? = null
     private var scrim: View? = null
     private val scrimAlpha = 0.8f
+    private var onClosedCallback: Runnable? = null
 
     override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0
 
@@ -141,7 +150,8 @@
 
     private fun populateAndShowForTask(
         taskContainer: TaskContainer,
-        alignedOptionIndex: Int
+        alignedOptionIndex: Int,
+        onClosedCallback: Runnable?
     ): Boolean {
         if (isAttachedToWindow) {
             return false
@@ -150,6 +160,7 @@
         taskView = taskContainer.taskView
         this.taskContainer = taskContainer
         this.alignedOptionIndex = alignedOptionIndex
+        this.onClosedCallback = onClosedCallback
         if (!populateMenu()) return false
         addScrim()
         show()
@@ -252,6 +263,7 @@
         super.closeComplete()
         popupContainer.removeView(scrim)
         popupContainer.removeView(iconView)
+        onClosedCallback?.run()
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index 56ca043..5dbc2ef 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -110,6 +110,7 @@
     private TaskView.FullscreenDrawParams mFullscreenParams;
     private ImageView mSplashView;
     private Drawable mSplashViewDrawable;
+    private TaskView mTaskView;
 
     @Nullable
     private Task mTask;
@@ -153,10 +154,11 @@
     /**
      * Updates the thumbnail to draw the provided task
      */
-    public void bind(Task task, TaskOverlay<?> overlay) {
+    public void bind(Task task, TaskOverlay<?> overlay, TaskView taskView) {
         mOverlay = overlay;
         mOverlay.reset();
         mTask = task;
+        mTaskView = taskView;
         int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
         mPaint.setColor(color);
         mBackgroundPaint.setColor(color);
@@ -292,8 +294,8 @@
 
     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
             float cornerRadius) {
-        if (mTask != null && getTaskView().isRunningTask()
-                && !getTaskView().getShouldShowScreenshot()) {
+        if (mTask != null && mTaskView.isRunningTask()
+                && !mTaskView.getShouldShowScreenshot()) {
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
                     mDimmingPaintAfterClearing);
@@ -334,10 +336,6 @@
         }
     }
 
-    public TaskView getTaskView() {
-        return (TaskView) getParent();
-    }
-
     public void setOverlayEnabled(boolean overlayEnabled) {
         if (mOverlayEnabled != overlayEnabled) {
             mOverlayEnabled = overlayEnabled;
@@ -390,9 +388,9 @@
         float viewCenterY = viewHeight / 2f;
         float centeredDrawableLeft = (viewWidth - drawableWidth) / 2f;
         float centeredDrawableTop = (viewHeight - drawableHeight) / 2f;
-        float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale();
-        float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null
-                ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen();
+        float nonGridScale = mTaskView == null ? 1 : 1 / mTaskView.getNonGridScale();
+        float recentsMaxScale = mTaskView == null || mTaskView.getRecentsView() == null
+                ? 1 : 1 / mTaskView.getRecentsView().getMaxScaleForFullScreen();
         float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX());
         float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY());
 
@@ -419,7 +417,7 @@
     }
 
     private boolean isThumbnailRotationDifferentFromTask() {
-        RecentsView recents = getTaskView().getRecentsView();
+        RecentsView recents = mTaskView.getRecentsView();
         if (recents == null || mThumbnailData == null) {
             return false;
         }
@@ -467,7 +465,7 @@
         if (mBitmapShader != null && mThumbnailData != null) {
             mPreviewRect.set(0, 0, mThumbnailData.getThumbnail().getWidth(),
                     mThumbnailData.getThumbnail().getHeight());
-            int currentRotation = getTaskView().getOrientedState().getRecentsActivityRotation();
+            int currentRotation = mTaskView.getOrientedState().getRecentsActivityRotation();
             boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
                     getMeasuredWidth(), getMeasuredHeight(), dp.isTablet, currentRotation, isRtl);
@@ -475,7 +473,7 @@
             mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix());
             mPaint.setShader(mBitmapShader);
         }
-        getTaskView().updateCurrentFullscreenParams();
+        mTaskView.updateCurrentFullscreenParams();
         invalidate();
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 004003c..b1cb407 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -46,14 +46,14 @@
 import androidx.core.view.updateLayoutParams
 import com.android.app.animation.Interpolators
 import com.android.launcher3.Flags.enableCursorHoverStates
-import com.android.launcher3.Flags.enableFocusOutline
 import com.android.launcher3.Flags.enableGridOnlyOverview
+import com.android.launcher3.Flags.enableHoverOfChildElementsInTaskview
+import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.anim.AnimatedFloat
-import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.testing.TestLogging
@@ -63,6 +63,7 @@
 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
@@ -76,12 +77,13 @@
 import com.android.launcher3.views.ActivityContext
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RemoteAnimationTargets
-import com.android.quickstep.TaskAnimationManager
 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.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.task.viewmodel.TaskViewModel
 import com.android.quickstep.util.ActiveGestureErrorDetector
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.util.BorderAnimator
@@ -105,7 +107,7 @@
     defStyleRes: Int = 0,
     focusBorderAnimator: BorderAnimator? = null,
     hoverBorderAnimator: BorderAnimator? = null,
-    type: TaskViewType = TaskViewType.SINGLE
+    private val type: TaskViewType = TaskViewType.SINGLE,
 ) : FrameLayout(context, attrs), ViewPool.Reusable {
     /**
      * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which
@@ -115,23 +117,30 @@
     @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
     annotation class TaskDataChanges
 
-    val taskViewData = TaskViewData(type)
+    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()
 
+    val taskIdSet: Set<Int>
+        /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
+        get() = taskContainers.map { it.task.key.id }.toSet()
+
     val snapshotViews: Array<View>
         get() = taskContainers.map { it.snapshotView }.toTypedArray()
 
     val isGridTask: Boolean
         /** Returns whether the task is part of overview grid and not being focused. */
-        get() = container.deviceProfile.isTablet && !isFocusedTask
+        get() = container.deviceProfile.isTablet && !isLargeTile
 
     val isRunningTask: Boolean
         get() = this === recentsView?.runningTaskView
 
-    val isFocusedTask: Boolean
-        get() = this === recentsView?.focusedTaskView
+    val isLargeTile: Boolean
+        get() =
+            this == recentsView?.focusedTaskView ||
+                (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP)
 
     val taskCornerRadius: Float
         get() = currentFullscreenParams.cornerRadius
@@ -190,14 +199,14 @@
         get() =
             pagedOrientationHandler.getPrimaryValue(
                 SPLIT_SELECT_TRANSLATION_X,
-                SPLIT_SELECT_TRANSLATION_Y
+                SPLIT_SELECT_TRANSLATION_Y,
             )
 
     protected val secondarySplitTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(
                 SPLIT_SELECT_TRANSLATION_X,
-                SPLIT_SELECT_TRANSLATION_Y
+                SPLIT_SELECT_TRANSLATION_Y,
             )
 
     protected val primaryDismissTranslationProperty: FloatProperty<TaskView>
@@ -212,21 +221,21 @@
         get() =
             pagedOrientationHandler.getPrimaryValue(
                 TASK_OFFSET_TRANSLATION_X,
-                TASK_OFFSET_TRANSLATION_Y
+                TASK_OFFSET_TRANSLATION_Y,
             )
 
     protected val secondaryTaskOffsetTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(
                 TASK_OFFSET_TRANSLATION_X,
-                TASK_OFFSET_TRANSLATION_Y
+                TASK_OFFSET_TRANSLATION_Y,
             )
 
     protected val taskResistanceTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(
                 TASK_RESISTANCE_TRANSLATION_X,
-                TASK_RESISTANCE_TRANSLATION_Y
+                TASK_RESISTANCE_TRANSLATION_Y,
             )
 
     private val tempCoordinates = FloatArray(2)
@@ -380,14 +389,32 @@
             applyTranslationX()
         }
 
-    protected var stableAlpha = 1f
+    private val taskViewAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+
+    protected var stableAlpha
         set(value) {
-            field = value
-            alpha = stableAlpha
+            taskViewAlpha.get(ALPHA_INDEX_STABLE).value = value
         }
+        get() = taskViewAlpha.get(ALPHA_INDEX_STABLE).value
+
+    var attachAlpha
+        set(value) {
+            taskViewAlpha.get(ALPHA_INDEX_ATTACH).value = value
+        }
+        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
 
     /** Enable or disable showing border on hover and focus change */
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
@@ -403,6 +430,26 @@
             focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true)
         }
 
+    /**
+     * Used to cache the hover border state so we don't repeatedly call the border animator with
+     * every hover event when the user hasn't crossed the threshold of the [thumbnailBounds].
+     */
+    private var hoverBorderVisible = false
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            Log.d(
+                TAG,
+                "${taskIds.contentToString()} - setting border animator visibility to: $field",
+            )
+            hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true)
+        }
+
+    // Used to cache thumbnail bounds to avoid recalculating on every hover move.
+    private var thumbnailBounds = Rect()
+
     private var focusTransitionProgress = 1f
         set(value) {
             field = value
@@ -415,7 +462,7 @@
             FOCUS_TRANSITION,
             FOCUS_TRANSITION_INDEX_COUNT,
             { x: Float, y: Float -> x * y },
-            1f
+            1f,
         )
     private val focusTransitionFullscreen =
         focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN)
@@ -441,27 +488,28 @@
 
     init {
         setOnClickListener { _ -> onClick() }
-        val keyboardFocusHighlightEnabled =
-            (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline())
+
+        if (enableRefactorTaskThumbnail()) {
+            taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type)
+        }
+
         val cursorHoverStatesEnabled = enableCursorHoverStates()
-        setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled)
+        setWillNotDraw(!cursorHoverStatesEnabled)
         context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use {
             this.focusBorderAnimator =
                 focusBorderAnimator
-                    ?: if (keyboardFocusHighlightEnabled)
-                        createSimpleBorderAnimator(
-                            currentFullscreenParams.cornerRadius.toInt(),
-                            context.resources.getDimensionPixelSize(
-                                R.dimen.keyboard_quick_switch_border_width
-                            ),
-                            { bounds: Rect -> getThumbnailBounds(bounds) },
-                            this,
-                            it.getColor(
-                                R.styleable.TaskView_focusBorderColor,
-                                BorderAnimator.DEFAULT_BORDER_COLOR
-                            )
-                        )
-                    else null
+                    ?: createSimpleBorderAnimator(
+                        currentFullscreenParams.cornerRadius.toInt(),
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.keyboard_quick_switch_border_width
+                        ),
+                        { bounds: Rect -> getThumbnailBounds(bounds) },
+                        this,
+                        it.getColor(
+                            R.styleable.TaskView_focusBorderColor,
+                            BorderAnimator.DEFAULT_BORDER_COLOR,
+                        ),
+                    )
             this.hoverBorderAnimator =
                 hoverBorderAnimator
                     ?: if (cursorHoverStatesEnabled)
@@ -474,8 +522,8 @@
                             this,
                             it.getColor(
                                 R.styleable.TaskView_hoverBorderColor,
-                                BorderAnimator.DEFAULT_BORDER_COLOR
-                            )
+                                BorderAnimator.DEFAULT_BORDER_COLOR,
+                            ),
                         )
                     else null
         }
@@ -485,7 +533,7 @@
     public override fun onFocusChanged(
         gainFocus: Boolean,
         direction: Int,
-        previouslyFocusedRect: Rect?
+        previouslyFocusedRect: Rect?,
     ) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
         if (borderEnabled) {
@@ -496,20 +544,28 @@
     override fun onHoverEvent(event: MotionEvent): Boolean {
         if (borderEnabled) {
             when (event.action) {
-                MotionEvent.ACTION_HOVER_ENTER ->
-                    hoverBorderAnimator?.setBorderVisibility(visible = true, animated = true)
-                MotionEvent.ACTION_HOVER_EXIT ->
-                    hoverBorderAnimator?.setBorderVisibility(visible = false, animated = true)
+                MotionEvent.ACTION_HOVER_ENTER -> {
+                    hoverBorderVisible =
+                        if (enableHoverOfChildElementsInTaskview()) {
+                            getThumbnailBounds(thumbnailBounds)
+                            event.isWithinThumbnailBounds()
+                        } else {
+                            true
+                        }
+                }
+                MotionEvent.ACTION_HOVER_MOVE ->
+                    if (enableHoverOfChildElementsInTaskview())
+                        hoverBorderVisible = event.isWithinThumbnailBounds()
+                MotionEvent.ACTION_HOVER_EXIT -> hoverBorderVisible = false
                 else -> {}
             }
         }
         return super.onHoverEvent(event)
     }
 
-    // avoid triggering hover event on child elements which would cause HOVER_EXIT for this
-    // task view
-    override fun onInterceptHoverEvent(event: MotionEvent) =
-        if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event)
+    override fun onInterceptHoverEvent(event: MotionEvent): Boolean =
+        if (enableHoverOfChildElementsInTaskview()) super.onInterceptHoverEvent(event)
+        else if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event)
 
     override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
         val recentsView = recentsView ?: return false
@@ -552,20 +608,24 @@
                 it.right = width
                 it.bottom = height
             }
+        if (enableHoverOfChildElementsInTaskview()) {
+            getThumbnailBounds(thumbnailBounds)
+        }
     }
 
     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()) {
-            notifyIsRunningTaskUpdated()
-        } else {
+        if (!enableRefactorTaskThumbnail()) {
             taskContainers.forEach { it.thumbnailViewDeprecated.setThumbnail(it.task, null) }
         }
         setOverlayEnabled(false)
         onTaskListVisibilityChanged(false)
         borderEnabled = false
+        hoverBorderVisible = false
         taskViewId = UNBOUND_TASK_VIEW_ID
         taskContainers.forEach { it.destroy() }
     }
@@ -576,24 +636,29 @@
     override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
         super.onInitializeAccessibilityNodeInfo(info)
         with(info) {
-            addAction(
-                AccessibilityAction(
-                    R.id.action_close,
-                    context.getText(R.string.accessibility_close)
+            // Only make actions available if the app icon menu is visible to the user.
+            // When modalness is >0, the user is in select mode and the icon menu is hidden.
+            if (modalness == 0f) {
+                addAction(
+                    AccessibilityAction(
+                        R.id.action_close,
+                        context.getText(R.string.accessibility_close),
+                    )
                 )
-            )
 
-            taskContainers.forEach {
-                TraceHelper.allowIpcs("TV.a11yInfo") {
-                    TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut ->
-                        addAction(shortcut.createAccessibilityAction(context))
+                taskContainers.forEach {
+                    TraceHelper.allowIpcs("TV.a11yInfo") {
+                        TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut
+                            ->
+                            addAction(shortcut.createAccessibilityAction(context))
+                        }
                     }
                 }
-            }
 
-            // Add DWB accessibility action at the end of the list
-            taskContainers.forEach {
-                it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction)
+                // Add DWB accessibility action at the end of the list
+                taskContainers.forEach {
+                    it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction)
+                }
             }
 
             recentsView?.let {
@@ -603,7 +668,7 @@
                         1,
                         it.taskViewCount - it.indexOfChild(this@TaskView) - 1,
                         1,
-                        false
+                        false,
                     )
             }
         }
@@ -636,7 +701,7 @@
     open fun bind(
         task: Task,
         orientedState: RecentsOrientedState,
-        taskOverlayFactory: TaskOverlayFactory
+        taskOverlayFactory: TaskOverlayFactory,
     ) {
         cancelPendingLoadTasks()
         taskContainers =
@@ -646,8 +711,9 @@
                     R.id.snapshot,
                     R.id.icon,
                     R.id.show_windows,
+                    R.id.digital_wellbeing_toast,
                     STAGE_POSITION_UNDEFINED,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 )
             )
         taskContainers.forEach { it.bind() }
@@ -659,19 +725,25 @@
         @IdRes thumbnailViewId: Int,
         @IdRes iconViewId: Int,
         @IdRes showWindowViewId: Int,
+        @IdRes digitalWellbeingBannerId: Int,
         @StagePosition stagePosition: Int,
-        taskOverlayFactory: TaskOverlayFactory
+        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)
         return TaskContainer(
@@ -681,9 +753,9 @@
             iconView,
             TransformingTouchDelegate(iconView.asView()),
             stagePosition,
-            DigitalWellBeingToast(container, this),
+            findViewById(digitalWellbeingBannerId)!!,
             findViewById(showWindowViewId)!!,
-            taskOverlayFactory
+            taskOverlayFactory,
         )
     }
 
@@ -719,7 +791,7 @@
     protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) {
         taskContainers.forEach {
             it.overlay.updateOrientationState(orientationState)
-            it.digitalWellBeingToast?.initialize(it.task)
+            it.digitalWellBeingToast?.initialize()
         }
     }
 
@@ -727,10 +799,10 @@
      * Updates TaskView scaling and translation required to support variable width if enabled, while
      * ensuring TaskView fits into screen in fullscreen.
      */
-    fun updateTaskSize(
+    open fun updateTaskSize(
         lastComputedTaskSize: Rect,
         lastComputedGridTaskSize: Rect,
-        lastComputedCarouselTaskSize: Rect
+        lastComputedCarouselTaskSize: Rect,
     ) {
         val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx
         val taskWidth = lastComputedTaskSize.width()
@@ -742,9 +814,10 @@
         if (container.deviceProfile.isTablet) {
             val boxWidth: Int
             val boxHeight: Int
-            if (isFocusedTask) {
-                // Task will be focused and should use focused task size. Use focusTaskRatio
-                // that is associated with the original orientation of the focused task.
+
+            // Focused task and Desktop tasks should use focusTaskRatio that is associated
+            // with the original orientation of the focused task.
+            if (isLargeTile) {
                 boxWidth = taskWidth
                 boxHeight = taskHeight
             } else {
@@ -770,10 +843,8 @@
         } else {
             nonGridScale = 1f
             boxTranslationY = 0f
-            expectedWidth = if (enableOverviewIconMenu()) taskWidth else LayoutParams.MATCH_PARENT
-            expectedHeight =
-                if (enableOverviewIconMenu()) taskHeight + thumbnailPadding
-                else LayoutParams.MATCH_PARENT
+            expectedWidth = taskWidth
+            expectedHeight = taskHeight + thumbnailPadding
         }
         this.nonGridScale = nonGridScale
         this.boxTranslationY = boxTranslationY
@@ -790,6 +861,7 @@
         taskContainers[0].snapshotView.updateLayoutParams<LayoutParams> {
             topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
         }
+        taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() }
     }
 
     /** Returns the thumbnail's bounds, optionally relative to the screen. */
@@ -801,7 +873,7 @@
             if (relativeToDragLayer) {
                 container.dragLayer.getDescendantRectRelativeToSelf(
                     it.snapshotView,
-                    thumbnailBounds
+                    thumbnailBounds,
                 )
             } else {
                 thumbnailBounds.set(it.snapshotView)
@@ -854,18 +926,11 @@
                             it.task.icon = icon
                             it.task.titleDescription = contentDescription
                             it.task.title = title
-                            setIcon(it.iconView, icon)
-                            if (enableOverviewIconMenu()) {
-                                setText(it.iconView, title)
-                            }
-                            it.digitalWellBeingToast?.initialize(it.task)
+                            onIconLoaded(it)
                         }
                         ?.also { request -> pendingIconLoadRequests.add(request) }
                 } else {
-                    setIcon(it.iconView, null)
-                    if (enableOverviewIconMenu()) {
-                        setText(it.iconView, null)
-                    }
+                    onIconUnloaded(it)
                 }
             }
         }
@@ -884,6 +949,21 @@
         pendingIconLoadRequests.clear()
     }
 
+    protected open fun onIconLoaded(taskContainer: TaskContainer) {
+        setIcon(taskContainer.iconView, taskContainer.task.icon)
+        if (enableOverviewIconMenu()) {
+            setText(taskContainer.iconView, taskContainer.task.title)
+        }
+        taskContainer.digitalWellBeingToast?.initialize()
+    }
+
+    protected open fun onIconUnloaded(taskContainer: TaskContainer) {
+        setIcon(taskContainer.iconView, null)
+        if (enableOverviewIconMenu()) {
+            setText(taskContainer.iconView, null)
+        }
+    }
+
     protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) {
         with(iconView) {
             if (icon != null) {
@@ -909,9 +989,14 @@
         iconView.setText(text)
     }
 
-    open fun refreshThumbnails(thumbnailDatas: HashMap<Int, ThumbnailData?>?) {
+    @JvmOverloads
+    open fun setShouldShowScreenshot(
+        shouldShowScreenshot: Boolean,
+        thumbnailDatas: Map<Int, ThumbnailData?>? = null,
+    ) {
+        if (this.shouldShowScreenshot == shouldShowScreenshot) return
+        this.shouldShowScreenshot = shouldShowScreenshot
         if (enableRefactorTaskThumbnail()) {
-            // TODO(b/342560598) add thumbnail logic
             return
         }
 
@@ -931,7 +1016,7 @@
             return
         }
         val callbackList =
-            launchTasks()?.apply {
+            launchWithAnimation()?.apply {
                 add {
                     Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted")
                 }
@@ -943,16 +1028,110 @@
             .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP)
     }
 
+    /** Launch of the current task (both live and inactive tasks) with an animation. */
+    fun launchWithAnimation(): RunnableList? {
+        return if (isRunningTask && recentsView?.remoteTargetHandles != null) {
+            launchAsLiveTile()
+        } else {
+            launchAsStaticTile()
+        }
+    }
+
+    private fun launchAsLiveTile(): RunnableList? {
+        val recentsView = recentsView ?: return null
+        val remoteTargetHandles = recentsView.remoteTargetHandles
+        if (!isClickableAsLiveTile) {
+            Log.e(
+                TAG,
+                "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}",
+            )
+            return null
+        }
+        isClickableAsLiveTile = false
+        val targets =
+            if (remoteTargetHandles.size == 1) {
+                remoteTargetHandles[0].transformParams.targetSet
+            } else {
+                val apps =
+                    remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() }
+                val wallpapers =
+                    remoteTargetHandles.flatMap {
+                        it.transformParams.targetSet.wallpapers.asIterable()
+                    }
+                RemoteAnimationTargets(
+                    apps.toTypedArray(),
+                    wallpapers.toTypedArray(),
+                    remoteTargetHandles[0].transformParams.targetSet.nonApps,
+                    remoteTargetHandles[0].transformParams.targetSet.targetMode,
+                )
+            }
+        if (targets == null) {
+            // If the recents animation is cancelled somehow between the parent if block and
+            // here, try to launch the task as a non live tile task.
+            val runnableList = launchAsStaticTile()
+            if (runnableList == null) {
+                Log.e(
+                    TAG,
+                    "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}",
+                )
+            }
+            isClickableAsLiveTile = true
+            return runnableList
+        }
+        TestLogging.recordEvent(
+            TestProtocol.SEQUENCE_MAIN,
+            "composeRecentsLaunchAnimator",
+            taskIds.contentToString(),
+        )
+        val runnableList = RunnableList()
+        with(AnimatorSet()) {
+            TaskViewUtils.composeRecentsLaunchAnimator(
+                this,
+                this@TaskView,
+                targets.apps,
+                targets.wallpapers,
+                targets.nonApps,
+                true /* launcherClosing */,
+                recentsView.stateManager,
+                recentsView,
+                recentsView.depthController,
+            )
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animator: Animator) {
+                        if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) {
+                            launchAsStaticTile()
+                        }
+                        isClickableAsLiveTile = true
+                        runEndCallback()
+                    }
+
+                    override fun onAnimationCancel(animation: Animator) {
+                        runEndCallback()
+                    }
+
+                    private fun runEndCallback() {
+                        runnableList.executeAllAndDestroy()
+                    }
+                }
+            )
+            start()
+        }
+        Log.d(TAG, "launchAsLiveTile - composeRecentsLaunchAnimator: ${taskIds.contentToString()}")
+        recentsView.onTaskLaunchedInLiveTileMode()
+        return runnableList
+    }
+
     /**
      * Starts the task associated with this view and animates the startup.
      *
      * @return CompletionStage to indicate the animation completion or null if the launch failed.
      */
-    open fun launchTaskAnimated(): RunnableList? {
+    open fun launchAsStaticTile(): RunnableList? {
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "startActivityFromRecentsAsync",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val opts =
             container.getActivityLaunchOptions(this, null).apply {
@@ -964,45 +1143,45 @@
         ) {
             Log.d(
                 TAG,
-                "launchTaskAnimated - startActivityFromRecents: ${taskIds.contentToString()}"
+                "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}",
             )
             ActiveGestureLog.INSTANCE.trackEvent(
                 ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED
             )
             val recentsView = recentsView ?: return null
-            if (recentsView.runningTaskViewId != -1) {
+            if (
+                recentsView.runningTaskViewId != -1 &&
+                    recentsView.mRecentsAnimationController != null
+            ) {
                 recentsView.onTaskLaunchedInLiveTileMode()
 
                 // Return a fresh callback in the live tile case, so that it's not accidentally
                 // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner.
                 return RunnableList().also { recentsView.addSideTaskLaunchCallback(it) }
             }
-            if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-                // If the recents transition is running (ie. in live tile mode), then the start
-                // of a new task will merge into the existing transition and it currently will
-                // not be run independently, so we need to rely on the onTaskAppeared() call
-                // for the new task to trigger the side launch callback to flush this runnable
-                // list (which is usually flushed when the app launch animation finishes)
-                recentsView.addSideTaskLaunchCallback(opts.onEndCallback)
-            }
+            // If the recents transition is running (ie. in live tile mode), then the start
+            // of a new task will merge into the existing transition and it currently will
+            // not be run independently, so we need to rely on the onTaskAppeared() call
+            // for the new task to trigger the side launch callback to flush this runnable
+            // list (which is usually flushed when the app launch animation finishes)
+            recentsView.addSideTaskLaunchCallback(opts.onEndCallback)
             return opts.onEndCallback
         } else {
-            notifyTaskLaunchFailed()
+            notifyTaskLaunchFailed("launchAsStaticTile")
             return null
         }
     }
 
     /** Starts the task associated with this view without any animation */
-    fun launchTask(callback: (launched: Boolean) -> Unit) {
-        launchTask(callback, isQuickSwitch = false)
-    }
-
-    /** Starts the task associated with this view without any animation */
-    open fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
+    @JvmOverloads
+    open fun launchWithoutAnimation(
+        isQuickSwitch: Boolean = false,
+        callback: (launched: Boolean) -> Unit,
+    ) {
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "startActivityFromRecentsAsync",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val firstContainer = taskContainers[0]
         val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext)
@@ -1011,7 +1190,7 @@
             // gesture launcher is in the background state, vs other launches which are in
             // the actual overview state
             failureListener.register(container, firstContainer.task.key.id) {
-                notifyTaskLaunchFailed()
+                notifyTaskLaunchFailed("launchWithoutAnimation")
                 recentsView?.let {
                     // Disable animations for now, as it is an edge case and the app usually
                     // covers launcher and also any state transition animation also gets
@@ -1033,7 +1212,7 @@
                     0,
                     0,
                     Executors.MAIN_EXECUTOR.handler,
-                    { callback(true) }
+                    { callback(true) },
                 ) {
                     failureListener.onTransitionFinished()
                 }
@@ -1042,11 +1221,7 @@
                     if (isQuickSwitch) {
                         setFreezeRecentTasksReordering()
                     }
-                    // TODO(b/334826842) add splash functionality to new TTV
-                    if (!enableRefactorTaskThumbnail()) {
-                        disableStartingWindow =
-                            firstContainer.thumbnailViewDeprecated.shouldShowSplashView()
-                    }
+                    disableStartingWindow = firstContainer.shouldShowSplashView
                 }
         Executors.UI_HELPER_EXECUTOR.execute {
             if (
@@ -1057,98 +1232,20 @@
                 // otherwise, wait for the animation start callback from the activity options
                 // above
                 Executors.MAIN_EXECUTOR.post {
-                    notifyTaskLaunchFailed()
+                    notifyTaskLaunchFailed("launchTask")
                     callback(false)
                 }
             }
-            Log.d(TAG, "launchTask - startActivityFromRecents: ${taskIds.contentToString()}")
+            Log.d(
+                TAG,
+                "launchWithoutAnimation - startActivityFromRecents: ${taskIds.contentToString()}",
+            )
         }
     }
 
-    /** Launch of the current task (both live and inactive tasks) with an animation. */
-    fun launchTasks(): RunnableList? {
-        val recentsView = recentsView ?: return null
-        val remoteTargetHandles = recentsView.mRemoteTargetHandles
-        if (!isRunningTask || remoteTargetHandles == null) {
-            return launchTaskAnimated()
-        }
-        if (!isClickableAsLiveTile) {
-            Log.e(TAG, "TaskView is not clickable as a live tile; returning to home.")
-            return null
-        }
-        isClickableAsLiveTile = false
-        val targets =
-            if (remoteTargetHandles.size == 1) {
-                remoteTargetHandles[0].transformParams.targetSet
-            } else {
-                val apps =
-                    remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() }
-                val wallpapers =
-                    remoteTargetHandles.flatMap {
-                        it.transformParams.targetSet.wallpapers.asIterable()
-                    }
-                RemoteAnimationTargets(
-                    apps.toTypedArray(),
-                    wallpapers.toTypedArray(),
-                    remoteTargetHandles[0].transformParams.targetSet.nonApps,
-                    remoteTargetHandles[0].transformParams.targetSet.targetMode
-                )
-            }
-        if (targets == null) {
-            // If the recents animation is cancelled somehow between the parent if block and
-            // here, try to launch the task as a non live tile task.
-            val runnableList = launchTaskAnimated()
-            if (runnableList == null) {
-                Log.e(
-                    TAG,
-                    "Recents animation cancelled and cannot launch task as non-live tile" +
-                        "; returning to home"
-                )
-            }
-            isClickableAsLiveTile = true
-            return runnableList
-        }
-        val runnableList = RunnableList()
-        with(AnimatorSet()) {
-            TaskViewUtils.composeRecentsLaunchAnimator(
-                this,
-                this@TaskView,
-                targets.apps,
-                targets.wallpapers,
-                targets.nonApps,
-                true /* launcherClosing */,
-                recentsView.stateManager,
-                recentsView,
-                recentsView.depthController
-            )
-            addListener(
-                object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animator: Animator) {
-                        if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) {
-                            launchTaskAnimated()
-                        }
-                        isClickableAsLiveTile = true
-                        runEndCallback()
-                    }
-
-                    override fun onAnimationCancel(animation: Animator) {
-                        runEndCallback()
-                    }
-
-                    private fun runEndCallback() {
-                        runnableList.executeAllAndDestroy()
-                    }
-                }
-            )
-            start()
-        }
-        Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: ${taskIds.contentToString()}")
-        recentsView.onTaskLaunchedInLiveTileMode()
-        return runnableList
-    }
-
-    private fun notifyTaskLaunchFailed() {
-        val sb = StringBuilder("Failed to launch task \n")
+    private fun notifyTaskLaunchFailed(launchMethod: String) {
+        val sb =
+            StringBuilder("$launchMethod - Failed to launch task: ${taskIds.contentToString()}\n")
         taskContainers.forEach {
             sb.append("(task=${it.task.key.baseIntent} userId=${it.task.key.userId})\n")
         }
@@ -1160,7 +1257,7 @@
         recentsView?.initiateSplitSelect(
             this,
             splitPositionOption.stagePosition,
-            SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition)
+            SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition),
         )
     }
 
@@ -1183,7 +1280,7 @@
             container.splitAnimationThumbnail,
             /* intent */ null,
             /* user */ null,
-            container.itemInfo
+            container.itemInfo,
         )
     }
 
@@ -1214,10 +1311,17 @@
 
     private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean {
         val recentsView = recentsView ?: return false
+        if (enableHoverOfChildElementsInTaskview()) {
+            // Disable hover on all TaskView's whilst menu is showing.
+            recentsView.setTaskBorderEnabled(false)
+        }
         return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) {
             menuContainer.iconView.revealAnim(/* isRevealing= */ true)
             TaskMenuView.showForTask(menuContainer) {
                 menuContainer.iconView.revealAnim(/* isRevealing= */ false)
+                if (enableHoverOfChildElementsInTaskview()) {
+                    recentsView.setTaskBorderEnabled(true)
+                }
             }
         } else if (container.deviceProfile.isTablet) {
             val alignedOptionIndex =
@@ -1237,9 +1341,17 @@
                 } else {
                     0
                 }
-            TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex)
+            TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) {
+                if (enableHoverOfChildElementsInTaskview()) {
+                    recentsView.setTaskBorderEnabled(true)
+                }
+            }
         } else {
-            TaskMenuView.showForTask(menuContainer)
+            TaskMenuView.showForTask(menuContainer) {
+                if (enableHoverOfChildElementsInTaskview()) {
+                    recentsView.setTaskBorderEnabled(true)
+                }
+            }
         }
     }
 
@@ -1262,7 +1374,7 @@
     private fun computeAndSetIconTouchDelegate(
         view: TaskViewIcon,
         tempCenterCoordinates: FloatArray,
-        transformingTouchDelegate: TransformingTouchDelegate
+        transformingTouchDelegate: TransformingTouchDelegate,
     ) {
         val viewHalfWidth = view.width / 2f
         val viewHalfHeight = view.height / 2f
@@ -1273,13 +1385,13 @@
                 this[0] = viewHalfWidth
                 this[1] = viewHalfHeight
             },
-            false
+            false,
         )
         transformingTouchDelegate.setBounds(
             (tempCenterCoordinates[0] - viewHalfWidth).toInt(),
             (tempCenterCoordinates[1] - viewHalfHeight).toInt(),
             (tempCenterCoordinates[0] + viewHalfWidth).toInt(),
-            (tempCenterCoordinates[1] + viewHalfHeight).toInt()
+            (tempCenterCoordinates[1] + viewHalfHeight).toInt(),
         )
     }
 
@@ -1289,7 +1401,7 @@
             it.showWindowsView?.let { showWindowsView ->
                 updateFilterCallback(
                     showWindowsView,
-                    getFilterUpdateCallback(it.task.key.packageName)
+                    getFilterUpdateCallback(it.task.key.packageName),
                 )
             }
         }
@@ -1325,7 +1437,7 @@
     private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) {
         taskContainers.forEach {
             it.iconView.setContentAlpha(focusTransitionProgress)
-            it.digitalWellBeingToast?.updateBannerOffset(1f - focusTransitionProgress)
+            it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - focusTransitionProgress
         }
     }
 
@@ -1358,7 +1470,7 @@
                 it.thumbnailViewDeprecated.dimAlpha = amount
             }
             it.iconView.setIconColorTint(tintColor, amount)
-            it.digitalWellBeingToast?.setBannerColorTint(tintColor, amount)
+            it.digitalWellBeingToast?.setColorTint(tintColor, amount)
         }
     }
 
@@ -1372,7 +1484,7 @@
         taskContainers.forEach {
             if (visibility == VISIBLE || it.task.key.id == taskId) {
                 it.snapshotView.visibility = visibility
-                it.digitalWellBeingToast?.setBannerVisibility(visibility)
+                it.digitalWellBeingToast?.visibility = visibility
                 it.showWindowsView?.visibility = visibility
                 it.overlay.setVisibility(visibility)
             }
@@ -1387,7 +1499,6 @@
 
     protected open fun refreshTaskThumbnailSplash() {
         if (!enableRefactorTaskThumbnail()) {
-            // TODO(b/334826842) add splash functionality to new TTV
             taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() }
         }
     }
@@ -1404,14 +1515,13 @@
         scaleX = scale
         scaleY = scale
         if (enableRefactorTaskThumbnail()) {
-            taskViewData.scale.value = scale
+            taskViewModel.updateScale(scale)
         }
         updateSnapshotRadius()
     }
 
     protected open fun applyThumbnailSplashAlpha() {
         if (!enableRefactorTaskThumbnail()) {
-            // TODO(b/334826842) add splash functionality to new TTV
             taskContainers.forEach {
                 it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha)
             }
@@ -1477,17 +1587,10 @@
     private fun onModalnessUpdated(modalness: Float) {
         taskContainers.forEach {
             it.iconView.setModalAlpha(1 - modalness)
-            it.digitalWellBeingToast?.updateBannerOffset(modalness)
+            it.digitalWellBeingToast?.bannerOffsetPercentage = modalness
         }
     }
 
-    /** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */
-    fun notifyIsRunningTaskUpdated() {
-        // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
-        //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
-        taskContainers.forEach { it.bindThumbnailView() }
-    }
-
     fun resetPersistentViewTransforms() {
         nonGridTranslationX = 0f
         gridTranslationX = 0f
@@ -1501,10 +1604,7 @@
         resetViewTransforms()
     }
 
-    fun getTaskContainerForTaskThumbnailView(taskThumbnailView: TaskThumbnailView): TaskContainer? =
-        taskContainers.firstOrNull { it.thumbnailView == taskThumbnailView }
-
-    open fun resetViewTransforms() {
+    fun resetViewTransforms() {
         // fullscreenTranslation and accumulatedTranslation should not be reset, as
         // resetViewTransforms is called during QuickSwitch scrolling.
         dismissTranslationX = 0f
@@ -1520,7 +1620,6 @@
         }
         dismissScale = 1f
         translationZ = 0f
-        alpha = stableAlpha
         setIconScaleAndDim(1f)
         setColorTint(0f, 0)
     }
@@ -1581,6 +1680,16 @@
         override fun close() {}
     }
 
+    private fun MotionEvent.isWithinThumbnailBounds(): Boolean {
+        return thumbnailBounds.contains(x.toInt(), y.toInt())
+    }
+
+    override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
+        (if (isLayoutRtl) taskContainers.reversed() else taskContainers).forEach {
+            it.addChildForAccessibility(outChildren)
+        }
+    }
+
     companion object {
         private const val TAG = "TaskView"
         const val FLAG_UPDATE_ICON = 1
@@ -1593,6 +1702,12 @@
         const val FOCUS_TRANSITION_INDEX_SCALE_AND_DIM = 1
         const val FOCUS_TRANSITION_INDEX_COUNT = 2
 
+        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 = 3
+
         /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
         const val MAX_PAGE_SCRIM_ALPHA = 0.4f
         const val SCALE_ICON_DURATION: Long = 120
@@ -1603,7 +1718,7 @@
             Interpolators.clampToProgress(
                 Interpolators.FAST_OUT_SLOW_IN,
                 1f - FOCUS_TRANSITION_THRESHOLD,
-                1f
+                1f,
             )!!
         private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect())
 
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 77%
rename from quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java
index c54862a..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,11 @@
 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;
 import java.util.Locale;
@@ -71,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.
      *
@@ -89,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);
     }
@@ -237,7 +215,8 @@
     /** An entire log of entries associated with a single log ID */
     protected static class EventLog {
 
-        protected final List<EventEntry> eventEntries = new ArrayList<>();
+        protected final List<EventEntry> eventEntries =
+                Collections.synchronizedList(new ArrayList<>());
         protected final int logId;
         protected final boolean mIsFullyGesturalNavMode;
 
@@ -250,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) {
@@ -281,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
@@ -364,10 +289,9 @@
 
         @Override
         public boolean equals(Object obj) {
-            if (!(obj instanceof CompoundString)) {
+            if (!(obj instanceof CompoundString other)) {
                 return false;
             }
-            CompoundString other = (CompoundString) obj;
             return (mIsNoOp == other.mIsNoOp)
                     && Objects.equals(mSubstrings, other.mSubstrings)
                     && Objects.equals(mArgs, other.mArgs);
diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
new file mode 100644
index 0000000..f43a125
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.MotionEvent.ACTION_DOWN;
+
+import static com.android.launcher3.Flags.enableActiveGestureProtoLog;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_CANCEL_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_START_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
+import static com.android.quickstep.util.QuickstepProtoLogGroup.ACTIVE_GESTURE_LOG;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+/**
+ * Proxy class used for ActiveGestureLog ProtoLog support.
+ * <p>
+ * This file will have all of its static strings in the
+ * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings.
+ * <p>
+ * When a new ActiveGestureLog entry needs to be added to the codebase (or and existing entry needs
+ * to be modified), add it here under a new unique method and make sure the ProtoLog entry matches
+ * to avoid confusion.
+ */
+public class ActiveGestureProtoLogProxy {
+
+    public static void logLauncherDestroyed() {
+        ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed");
+    }
+
+    public static void logAbsSwipeUpHandlerOnRecentsAnimationCanceled() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "AbsSwipeUpHandler.onRecentsAnimationCanceled",
+                /* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onRecentsAnimationCanceled");
+    }
+
+    public static void logAbsSwipeUpHandlerOnRecentsAnimationFinished() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "RecentsAnimationCallbacks.onAnimationFinished",
+                ON_FINISH_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onAnimationFinished");
+    }
+
+    public static void logAbsSwipeUpHandlerCancelCurrentAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(
+                "AbsSwipeUpHandler.cancelCurrentAnimation",
+                ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.cancelCurrentAnimation");
+    }
+
+    public static void logAbsSwipeUpHandlerOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.onTasksAppeared: "
+                + "force finish recents animation complete; clearing state callback.");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onTasksAppeared: "
+                + "force finish recents animation complete; clearing state callback.");
+    }
+
+    public static void logFinishRecentsAnimationOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimationOnTasksAppeared");
+    }
+
+    public static void logRecentsAnimationCallbacksOnAnimationCancelled() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled",
+                /* gestureEvent= */ ON_CANCEL_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationCanceled");
+    }
+
+    public static void logRecentsAnimationCallbacksOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared",
+                ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onTasksAppeared");
+    }
+
+    public static void logStartRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "TaskAnimationManager.startRecentsAnimation",
+                /* gestureEvent= */ START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TaskAnimationManager.startRecentsAnimation");
+    }
+
+    public static void logLaunchingSideTaskFailed() {
+        ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Unable to launch side task (no recents)");
+    }
+
+    public static void logContinueRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "continueRecentsAnimation");
+    }
+
+    public static void logCleanUpRecentsAnimationSkipped() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation skipped due to wrong callbacks");
+    }
+
+    public static void logCleanUpRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation");
+    }
+
+    public static void logOnInputEventUserLocked() {
+        ActiveGestureLog.INSTANCE.addLog(
+                "TIS.onInputEvent: Cannot process input event: user is locked");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Cannot process input event: user is locked");
+    }
+
+    public static void logOnInputIgnoringFollowingEvents() {
+        ActiveGestureLog.INSTANCE.addLog("TIS.onMotionEvent: A new gesture has been started, "
+                        + "but a previously-requested recents animation hasn't started. "
+                        + "Ignoring all following motion events.",
+                RECENTS_ANIMATION_START_PENDING);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: A new gesture has been started, "
+                + "but a previously-requested recents animation hasn't started. "
+                + "Ignoring all following motion events.");
+    }
+
+    public static void logOnInputEventThreeButtonNav() {
+        ActiveGestureLog.INSTANCE.addLog("TIS.onInputEvent: Cannot process input event: "
+                + "using 3-button nav and event is not a trackpad event");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onInputEvent: Cannot process input event: "
+                + "using 3-button nav and event is not a trackpad event");
+    }
+
+    public static void logPreloadRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog("preloadRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "preloadRecentsAnimation");
+    }
+
+    public static void logRecentTasksMissing() {
+        ActiveGestureLog.INSTANCE.addLog("Null mRecentTasks", RECENT_TASKS_MISSING);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Null mRecentTasks");
+    }
+
+    public static void logExecuteHomeCommand() {
+        ActiveGestureLog.INSTANCE.addLog("OverviewCommandHelper.executeCommand(HOME)");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewCommandHelper.executeCommand(HOME)");
+    }
+
+    public static void logFinishRecentsAnimationCallback() {
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation-callback");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation-callback");
+    }
+
+    public static void logOnScrollerAnimationAborted() {
+        ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
+                ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "scroller animation aborted");
+    }
+
+    public static void logInputConsumerBecameActive(@NonNull String consumerName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "%s became active", consumerName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "%s became active", consumerName);
+    }
+
+    public static void logTaskLaunchFailed(int launchedTaskId) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launch failed, task (id=%d) finished mid transition", launchedTaskId));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Launch failed, task (id=%d) finished mid transition", launchedTaskId);
+    }
+
+    public static void logOnPageEndTransition(int nextPageIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onPageEndTransition: current page index updated: %d", nextPageIndex));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onPageEndTransition: current page index updated: %d", nextPageIndex);
+    }
+
+    public static void logQuickSwitchFromHomeFallback(int taskIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Quick switch from home fallback case: The TaskView at index %d is missing.",
+                        taskIndex),
+                QUICK_SWITCH_FROM_HOME_FALLBACK);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Quick switch from home fallback case: The TaskView at index %d is missing.",
+                taskIndex);
+    }
+
+    public static void logQuickSwitchFromHomeFailed(int taskIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Quick switch from home failed: TaskViews at indices %d and 0 are missing.",
+                        taskIndex),
+                QUICK_SWITCH_FROM_HOME_FAILED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Quick switch from home failed: TaskViews at indices %d and 0 are missing.",
+                taskIndex);
+    }
+
+    public static void logFinishRecentsAnimation(boolean toRecents) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "finishRecentsAnimation: %b", toRecents),
+                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation: %b", toRecents);
+    }
+
+    public static void logSetEndTarget(@NonNull String target) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "setEndTarget %s", target), /* gestureEvent= */ SET_END_TARGET);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "setEndTarget %s", target);
+    }
+
+    public static void logStartHomeIntent(@NonNull String reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "OverviewComponentObserver.startHomeIntent: %s", reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewComponentObserver.startHomeIntent: %s", reason);
+    }
+
+    public static void logRunningTaskPackage(@NonNull String packageName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Current running task package name=%s", packageName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Current running task package name=%s", packageName);
+    }
+
+    public static void logSysuiStateFlags(@NonNull String stateFlags) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Current SystemUi state flags=%s", stateFlags));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Current SystemUi state flags=%s", stateFlags);
+    }
+
+    public static void logSetInputConsumer(@NonNull String consumerName, @NonNull String reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "setInputConsumer: %s. reason(s):%s", consumerName, reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "setInputConsumer: %s. reason(s):%s", consumerName, reason);
+    }
+
+    public static void logUpdateGestureStateRunningTask(
+            @NonNull String otherTaskPackage, @NonNull String runningTaskPackage) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Changing active task to %s because the previous task running on top of this "
+                        + "one (%s) was excluded from recents",
+                otherTaskPackage,
+                runningTaskPackage));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Changing active task to %s because the previous task running on top of this "
+                        + "one (%s) was excluded from recents",
+                otherTaskPackage,
+                runningTaskPackage);
+    }
+
+    public static void logOnInputEventActionUp(
+            int x, int y, int action, @NonNull String classification) {
+        String actionString = MotionEvent.actionToString(action);
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onMotionEvent(%d, %d): %s, %s", x, y, actionString, classification),
+                /* gestureEvent= */ action == ACTION_DOWN
+                        ? MOTION_DOWN
+                        : MOTION_UP);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onMotionEvent(%d, %d): %s, %s", x, y, actionString, classification);
+    }
+
+    public static void logOnInputEventActionMove(
+            @NonNull String action, @NonNull String classification, int pointerCount) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                        "onMotionEvent: %s, %s, pointerCount: %d",
+                        action,
+                        classification,
+                        pointerCount),
+                MOTION_MOVE);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onMotionEvent: %s, %s, pointerCount: %d", action, classification, pointerCount);
+    }
+
+    public static void logOnInputEventGenericAction(
+            @NonNull String action, @NonNull String classification) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onMotionEvent: %s, %s", action, classification));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "onMotionEvent: %s, %s", action, classification);
+    }
+
+    public static void logOnInputEventNavModeSwitched(
+            @NonNull String startNavMode, @NonNull String currentNavMode) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onInputEvent: Navigation mode switched mid-gesture (%s -> %s); "
+                        + "cancelling gesture.",
+                        startNavMode,
+                        currentNavMode),
+                NAVIGATION_MODE_SWITCHED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Navigation mode switched mid-gesture (%s -> %s); "
+                        + "cancelling gesture.",
+                startNavMode,
+                currentNavMode);
+    }
+
+    public static void logUnknownInputEvent(@NonNull String event) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onInputEvent: Cannot process input event: received unknown event %s", event));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Cannot process input event: received unknown event %s", event);
+    }
+
+    public static void logFinishRunningRecentsAnimation(boolean toHome) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "finishRunningRecentsAnimation: %b", toHome));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRunningRecentsAnimation: %b", toHome);
+    }
+
+    public static void logOnRecentsAnimationStartCancelled() {
+        ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onAnimationStart (canceled): 0",
+                /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationStart (canceled): 0");
+    }
+
+    public static void logOnRecentsAnimationStart(int appCount) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "RecentsAnimationCallbacks.onAnimationStart (canceled): %d", appCount),
+                /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "RecentsAnimationCallbacks.onAnimationStart (canceled): %d", appCount);
+    }
+
+    public static void logStartRecentsAnimationCallback(@NonNull String callback) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TaskAnimationManager.startRecentsAnimation(%s): "
+                        + "Setting mRecentsAnimationStartPending = false",
+                callback));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TaskAnimationManager.startRecentsAnimation(%s): "
+                        + "Setting mRecentsAnimationStartPending = false",
+                callback);
+    }
+
+    public static void logSettingRecentsAnimationStartPending(boolean value) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TaskAnimationManager.startRecentsAnimation: "
+                        + "Setting mRecentsAnimationStartPending = %b",
+                value));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TaskAnimationManager.startRecentsAnimation: "
+                        + "Setting mRecentsAnimationStartPending = %b",
+                value);
+    }
+
+    public static void logLaunchingSideTask(int taskId) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launching side task id=%d", taskId));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Launching side task id=%d", taskId);
+    }
+
+    public static void logOnInputEventActionDown(@NonNull ActiveGestureLog.CompoundString reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onMotionEvent: ").append(reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", reason.toString());
+    }
+
+    public static void logStartNewTask(@NonNull ActiveGestureLog.CompoundString tasks) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launching task: ").append(tasks));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", tasks.toString());
+    }
+
+    public static void logMotionPauseDetectorEvent(@NonNull ActiveGestureLog.CompoundString event) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "MotionPauseDetector: ").append(event));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "MotionPauseDetector: %s", event.toString());
+    }
+
+    public static void logHandleTaskAppearedFailed(
+            @NonNull ActiveGestureLog.CompoundString reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "handleTaskAppeared check failed: ").append(reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "handleTaskAppeared check failed: %s", reason.toString());
+    }
+
+    /**
+     * This is for special cases where the string is purely dynamic and therefore has no format that
+     * can be extracted. Do not use in any other case.
+     */
+    public static void logDynamicString(
+            @NonNull String string,
+            @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
+        ActiveGestureLog.INSTANCE.addLog(string, gestureEvent);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "%s", string);
+    }
+
+    public static void logOnSettledOnEndTarget(@NonNull String endTarget) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onSettledOnEndTarget %s", endTarget),
+                /* gestureEvent= */ ON_SETTLED_ON_END_TARGET);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "onSettledOnEndTarget %s", endTarget);
+    }
+
+    public static void logOnCalculateEndTarget(float velocityX, float velocityY, double angle) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f",
+                        velocityX,
+                        velocityY,
+                        angle),
+                velocityX == 0 && velocityY == 0 ? INVALID_VELOCITY_ON_SWIPE_UP : null);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f",
+                velocityX,
+                velocityY,
+                angle);
+    }
+
+    public static void logUnexpectedTaskAppeared(int taskId, @NonNull String packageName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s",
+                taskId,
+                packageName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s",
+                taskId,
+                packageName);
+    }
+}
diff --git a/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java
new file mode 100644
index 0000000..bb02a11
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+import java.util.UUID;
+
+/** Enums used to interface with the ProtoLog API. */
+public enum QuickstepProtoLogGroup implements IProtoLogGroup {
+
+    ACTIVE_GESTURE_LOG(true, true, false, "ActiveGestureLog"),
+    RECENTS_WINDOW(true, true, Constants.DEBUG_RECENTS_WINDOW, "RecentsWindow"),
+    LAUNCHER_STATE_MANAGER(true, true, Constants.DEBUG_STATE_MANAGER, "LauncherStateManager");
+
+    private final boolean mEnabled;
+    private volatile boolean mLogToProto;
+    private volatile boolean mLogToLogcat;
+    private final @NonNull String mTag;
+
+    public static void initProtoLog() {
+        ProtoLog.init(QuickstepProtoLogGroup.values());
+    }
+
+    /**
+     * @param enabled     set to false to exclude all log statements for this group from
+     *                    compilation,
+     *                    they will not be available in runtime.
+     * @param logToProto  enable binary logging for the group
+     * @param logToLogcat enable text logging for the group
+     * @param tag         name of the source of the logged message
+     */
+    QuickstepProtoLogGroup(
+            boolean enabled, boolean logToProto, boolean logToLogcat, @NonNull String tag) {
+        this.mEnabled = enabled;
+        this.mLogToProto = logToProto;
+        this.mLogToLogcat = logToLogcat;
+        this.mTag = tag;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    @Override
+    public boolean isLogToProto() {
+        return mLogToProto;
+    }
+
+    @Override
+    public boolean isLogToLogcat() {
+        return mLogToLogcat;
+    }
+
+    @Override
+    public boolean isLogToAny() {
+        return mLogToLogcat || mLogToProto;
+    }
+
+    @Override
+    public int getId() {
+        return Constants.LOG_START_ID + this.ordinal();
+    }
+
+    @Override
+    public @NonNull String getTag() {
+        return mTag;
+    }
+
+    @Override
+    public void setLogToProto(boolean logToProto) {
+        this.mLogToProto = logToProto;
+    }
+
+    @Override
+    public void setLogToLogcat(boolean logToLogcat) {
+        this.mLogToLogcat = logToLogcat;
+    }
+
+    private static final class Constants {
+
+        private static final boolean DEBUG_RECENTS_WINDOW = false;
+        private static final boolean DEBUG_STATE_MANAGER = true; // b/279059025, b/325463989
+
+        private static final int LOG_START_ID =
+                (int) (UUID.nameUUIDFromBytes(QuickstepProtoLogGroup.class.getName().getBytes())
+                        .getMostSignificantBits() % Integer.MAX_VALUE);
+    }
+}
diff --git a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
new file mode 100644
index 0000000..f54ad67
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static com.android.launcher3.Flags.enableRecentsWindowProtoLog;
+import static com.android.quickstep.util.QuickstepProtoLogGroup.RECENTS_WINDOW;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+/**
+ * Proxy class used for Recents Window ProtoLog support.
+ * <p>
+ * This file will have all of its static strings in the
+ * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings.
+ * <p>
+ * When a new Recents Window log needs to be added to the codebase, add it here under a new unique
+ * method. Or, if an existing entry needs to be modified, simply update it here.
+ */
+public class RecentsWindowProtoLogProxy {
+
+    public static void logOnStateSetStart(@NonNull String stateName) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW, "onStateSetStart: %s", stateName);
+    }
+
+    public static void logOnStateSetEnd(@NonNull String stateName) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW, "onStateSetEnd: %s", stateName);
+    }
+
+    public static void logStartRecentsWindow(boolean isShown, boolean windowViewIsNull) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW,
+                "Starting recents window: isShow= %b, windowViewIsNull=%b",
+                isShown,
+                windowViewIsNull);
+    }
+
+    public static void logCleanup(boolean isShown) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW, "Cleaning up recents window: isShow= %b", isShown);
+    }
+}
diff --git a/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt
new file mode 100644
index 0000000..2f1f0b5
--- /dev/null
+++ b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.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.launcher3.taskbar.bubbles.testing
+
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.PathParser
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
+import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.wm.shell.shared.bubbles.BubbleInfo
+
+object FakeBubbleViewFactory {
+
+    /** Inflates a [BubbleView] and adds it to the [parent] view if it is present. */
+    fun createBubble(
+        context: Context,
+        key: String,
+        parent: ViewGroup?,
+        iconSize: Int = 50,
+        iconColor: Int,
+        badgeColor: Int = Color.RED,
+        dotColor: Int = Color.BLUE,
+        suppressNotification: Boolean = false,
+    ): BubbleView {
+        val inflater = LayoutInflater.from(context)
+        // BubbleView uses launcher's badge to icon ratio and expects the badge image to already
+        // have the right size
+        val badgeToIconRatio = 0.444f
+        val badgeRadius = iconSize * badgeToIconRatio / 2
+        val icon = createCircleBitmap(radius = iconSize / 2, color = iconColor)
+        val badge = createCircleBitmap(radius = badgeRadius.toInt(), color = badgeColor)
+
+        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,
+                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",
+                null,
+            )
+        bubbleView.setBubble(bubble)
+        return bubbleView
+    }
+
+    private fun createCircleBitmap(radius: Int, color: Int): Bitmap {
+        val bitmap = Bitmap.createBitmap(radius * 2, radius * 2, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(bitmap)
+        canvas.drawARGB(0, 0, 0, 0)
+        val paint = Paint()
+        paint.color = color
+        canvas.drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), paint)
+        return bitmap
+    }
+}
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
new file mode 100644
index 0000000..b5a418b
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.content.Context
+import android.graphics.Color
+import android.platform.test.rule.ScreenRecordRule
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.FrameLayout.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
+import androidx.activity.ComponentActivity
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.bubbles.testing.FakeBubbleViewFactory
+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 [BubbleBarView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+@ScreenRecordRule.ScreenRecord
+class BubbleBarViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var bubbleBarView: BubbleBarView
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() =
+            DeviceEmulationSpec.forDisplays(
+                Displays.Phone,
+                isDarkTheme = false,
+                isLandscape = false,
+            )
+    }
+
+    @get:Rule
+    val screenshotRule =
+        ViewScreenshotTestRule(
+            emulationSpec,
+            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
+        )
+
+    @Test
+    fun bubbleBarView_collapsed_oneBubble() {
+        screenshotRule.screenshotTest("bubbleBarView_collapsed_oneBubble") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleBarView()
+            bubbleBarView.addBubble(createBubble("key1", Color.GREEN))
+            val container = FrameLayout(context)
+            val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+            container.layoutParams = lp
+            container.addView(bubbleBarView)
+            container
+        }
+    }
+
+    @Test
+    fun bubbleBarView_collapsed_twoBubbles() {
+        screenshotRule.screenshotTest("bubbleBarView_collapsed_twoBubbles") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleBarView()
+            bubbleBarView.addBubble(createBubble("key1", Color.GREEN))
+            bubbleBarView.addBubble(createBubble("key2", Color.CYAN))
+            val container = FrameLayout(context)
+            val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+            container.layoutParams = lp
+            container.addView(bubbleBarView)
+            container
+        }
+    }
+
+    @Test
+    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,
+        ) { activity ->
+            activity.actionBar?.hide()
+            setupBubbleBarView()
+            bubbleBarView.addBubble(createBubble("key1", Color.GREEN))
+            bubbleBarView.addBubble(createBubble("key2", Color.CYAN))
+            bubbleBarView.addBubble(createBubble("key3", Color.MAGENTA))
+            val container = FrameLayout(context)
+            val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+            container.layoutParams = lp
+            container.addView(bubbleBarView)
+            bubbleBarView.isExpanded = true
+            container
+        }
+    }
+
+    private fun setupBubbleBarView() {
+        bubbleBarView = BubbleBarView(context)
+        val lp = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+        bubbleBarView.layoutParams = lp
+        val paddingTop =
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_pointer_visible_size)
+        bubbleBarView.setPadding(0, paddingTop, 0, 0)
+        bubbleBarView.visibility = View.VISIBLE
+        bubbleBarView.alpha = 1f
+    }
+
+    private fun createBubble(key: String, color: Int): BubbleView {
+        val bubbleView =
+            FakeBubbleViewFactory.createBubble(
+                context,
+                key,
+                parent = bubbleBarView,
+                iconColor = color,
+            )
+        bubbleView.showDotIfNeeded(1f)
+        bubbleBarView.setSelectedBubble(bubbleView)
+        return bubbleView
+    }
+}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
new file mode 100644
index 0000000..47f393f
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.content.Context
+import android.graphics.Color
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.taskbar.bubbles.testing.FakeBubbleViewFactory
+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 [BubbleView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() =
+            DeviceEmulationSpec.forDisplays(
+                Displays.Phone,
+                isDarkTheme = false,
+                isLandscape = false,
+            )
+    }
+
+    @get:Rule
+    val screenshotRule =
+        ViewScreenshotTestRule(
+            emulationSpec,
+            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
+        )
+
+    @Test
+    fun bubbleView_hasUnseenContent() {
+        screenshotRule.screenshotTest("bubbleView_hasUnseenContent") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleView()
+        }
+    }
+
+    @Test
+    fun bubbleView_seen() {
+        screenshotRule.screenshotTest("bubbleView_seen") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleView(suppressNotification = true)
+        }
+    }
+
+    @Test
+    fun bubbleView_badgeHidden() {
+        screenshotRule.screenshotTest("bubbleView_badgeHidden") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleView().apply { setBadgeScale(0f) }
+        }
+    }
+
+    private fun setupBubbleView(suppressNotification: Boolean = false): BubbleView {
+        val bubbleView =
+            FakeBubbleViewFactory.createBubble(
+                context,
+                key = "key",
+                parent = null,
+                iconSize = 100,
+                iconColor = Color.LTGRAY,
+                suppressNotification = suppressNotification,
+            )
+        bubbleView.showDotIfNeeded(1f)
+        return bubbleView
+    }
+}
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
new file mode 100644
index 0000000..11c7fe9
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt
@@ -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.launcher3.taskbar.bubbles.flyout
+
+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
+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 [BubbleBarFlyoutView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() =
+            DeviceEmulationSpec.forDisplays(
+                Displays.Phone,
+                isDarkTheme = false,
+                isLandscape = false,
+            )
+    }
+
+    @get:Rule
+    val screenshotRule =
+        ViewScreenshotTestRule(
+            emulationSpec,
+            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
+        )
+
+    @Test
+    fun bubbleBarFlyoutView_noAvatar_onRight() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onRight") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message")
+            ) {}
+            flyout.updateExpansionProgress(1f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_noAvatar_onLeft() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onLeft") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message")
+            ) {}
+            flyout.updateExpansionProgress(1f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_noAvatar_longMessage() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_longMessage") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = null,
+                    title = "sender",
+                    message = "really, really, really, really, really long message. like really.",
+                )
+            ) {}
+            flyout.updateExpansionProgress(1f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_avatar_onRight() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onRight") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "message",
+                )
+            ) {}
+            flyout.updateExpansionProgress(1f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_avatar_onLeft() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onLeft") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "message",
+                )
+            ) {}
+            flyout.updateExpansionProgress(1f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_avatar_longMessage() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_longMessage") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "really, really, really, really, really long message. like really.",
+                )
+            ) {}
+            flyout.updateExpansionProgress(1f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_collapsed_onLeft() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onLeft") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "collapsed on left",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_collapsed_onRight() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onRight") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "collapsed on right",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_90p_onLeft() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_90p_onLeft") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(
+                    context,
+                    FakeBubbleBarFlyoutPositioner(
+                        isOnLeft = true,
+                        distanceToCollapsedPosition = PointF(100f, 100f),
+                    ),
+                )
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "expanded 90% on left",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0.9f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_80p_onRight() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_80p_onRight") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(
+                    context,
+                    FakeBubbleBarFlyoutPositioner(
+                        isOnLeft = false,
+                        distanceToCollapsedPosition = PointF(200f, 100f),
+                    ),
+                )
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "expanded 80% on right",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0.8f)
+            flyout
+        }
+    }
+
+    private class FakeBubbleBarFlyoutPositioner(
+        override val isOnLeft: Boolean,
+        override val distanceToCollapsedPosition: PointF = PointF(0f, 0f),
+    ) : BubbleBarFlyoutPositioner {
+        override val targetTy = 0f
+        override val collapsedSize = 30f
+        override val collapsedColor = Color.BLUE
+        override val collapsedElevation = 1f
+        override val distanceToRevealTriangle = 10f
+    }
+}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
new file mode 100644
index 0000000..ff5d8bd
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Matrix
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
+    override val cornerRadiusProgress = MutableStateFlow(0f)
+    override val inheritedScale = MutableStateFlow(1f)
+    override val dimProgress = MutableStateFlow(0f)
+    override val splashAlpha = MutableStateFlow(0f)
+    override val uiState = MutableStateFlow<TaskThumbnailUiState>(Uninitialized)
+
+    override fun bind(taskId: Int) {
+        // no-op
+    }
+
+    override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean) =
+        Matrix.IDENTITY_MATRIX
+}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
new file mode 100644
index 0000000..75769e9
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.task.thumbnail
+
+import android.content.Context
+import android.graphics.Color
+import android.view.LayoutInflater
+import com.android.launcher3.R
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.ViewScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** Screenshot tests for [TaskThumbnailView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+
+    @get:Rule
+    val screenshotRule =
+        ViewScreenshotTestRule(
+            emulationSpec,
+            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
+        )
+
+    private val taskThumbnailViewModel = FakeTaskThumbnailViewModel()
+
+    @Test
+    fun taskThumbnailView_uninitialized() {
+        screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity)
+        }
+    }
+
+    @Test
+    fun taskThumbnailView_backgroundOnly() {
+        screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity ->
+            activity.actionBar?.hide()
+            taskThumbnailViewModel.uiState.value = TaskThumbnailUiState.BackgroundOnly(Color.YELLOW)
+            createTaskThumbnailView(activity)
+        }
+    }
+
+    private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
+        val di = RecentsDependencies.initialize(context)
+        val taskThumbnailView =
+            LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
+        val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
+        di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
+        di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
+
+        return taskThumbnailView as TaskThumbnailView
+    }
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() =
+            DeviceEmulationSpec.forDisplays(
+                Displays.Phone,
+                isDarkTheme = false,
+                isLandscape = false,
+            )
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
similarity index 79%
rename from quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
index a532762..0005df6 100644
--- a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
@@ -1,3 +1,18 @@
+/*
+ * 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.app.prediction.AppPredictor
@@ -19,7 +34,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
 
 /** Unit tests for [QuickstepModelDelegate]. */
@@ -57,25 +72,25 @@
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_PREDICTION)
 
         verify(allAppsPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
-        verifyZeroInteractions(hotseatPredictor)
-        verifyZeroInteractions(widgetRecommendationPredictor)
+        verifyNoMoreInteractions(hotseatPredictor)
+        verifyNoMoreInteractions(widgetRecommendationPredictor)
     }
 
     @Test
     fun onWidgetPrediction_notifyWidgetRecommendationPredictor() {
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WIDGETS_PREDICTION)
 
-        verifyZeroInteractions(allAppsPredictor)
+        verifyNoMoreInteractions(allAppsPredictor)
         verify(widgetRecommendationPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
-        verifyZeroInteractions(hotseatPredictor)
+        verifyNoMoreInteractions(hotseatPredictor)
     }
 
     @Test
     fun onHotseatPrediction_notifyHotseatPredictor() {
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
 
-        verifyZeroInteractions(allAppsPredictor)
-        verifyZeroInteractions(widgetRecommendationPredictor)
+        verifyNoMoreInteractions(allAppsPredictor)
+        verifyNoMoreInteractions(widgetRecommendationPredictor)
         verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
     }
 
@@ -83,8 +98,8 @@
     fun onOtherClient_notifyHotseatPredictor() {
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WALLPAPERS)
 
-        verifyZeroInteractions(allAppsPredictor)
-        verifyZeroInteractions(widgetRecommendationPredictor)
+        verifyNoMoreInteractions(allAppsPredictor)
+        verifyNoMoreInteractions(widgetRecommendationPredictor)
         verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
     }
 
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
new file mode 100644
index 0000000..6e2f74a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 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
new file mode 100644
index 0000000..455b6c5
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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 com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+class TaskbarDesktopModeControllerTest {
+
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @TaskbarUnitTestRule.InjectController
+    lateinit var taskbarDesktopModeController: TaskbarDesktopModeController
+
+    @Test
+    fun whenTaskbarRequiresCornerRoundness_shouldReturnDefaultCornerRoundness() {
+        assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(true))
+            .isEqualTo(MAX_ROUNDNESS)
+    }
+
+    @Test
+    fun whenTaskbarRequiresCornerRoundness_shouldReturnZeroAsCornerRoundness() {
+        assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(false)).isEqualTo(0f)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
new file mode 100644
index 0000000..3c80352
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
@@ -0,0 +1,187 @@
+/*
+ * 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 com.android.launcher3.Utilities
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+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.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.launcher3.util.OnboardingPrefs
+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)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+class TaskbarEduTooltipControllerTest {
+
+    @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 taskbarEduTooltipController: TaskbarEduTooltipController
+
+    private val taskbarContext: TaskbarActivityContext
+        get() = taskbarUnitTestRule.activityContext
+
+    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() {
+        Utilities.disableRunningInTestHarnessForTests()
+    }
+
+    @After
+    fun tearDown() {
+        if (wasInTestHarness) {
+            Utilities.enableRunningInTestHarnessForTests()
+        }
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testMaybeShowSwipeEdu_whenTaskbarIsInThreeButtonMode_doesNotShowSwipeEdu() {
+        tooltipStep = TOOLTIP_STEP_SWIPE
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
+        runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testMaybeShowSwipeEdu_whenSwipeEduAlreadyShown_doesNotShowSwipeEdu() {
+        tooltipStep = TOOLTIP_STEP_FEATURES
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
+        runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testMaybeShowSwipeEdu_whenUserHasNotSeen_doesShowSwipeEdu() {
+        tooltipStep = TOOLTIP_STEP_SWIPE
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
+        runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testMaybeShowFeaturesEdu_whenFeatureEduAlreadyShown_doesNotShowFeatureEdu() {
+        tooltipStep = TOOLTIP_STEP_NONE
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
+        runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testMaybeShowFeaturesEdu_whenUserHasNotSeen_doesShowFeatureEdu() {
+        tooltipStep = TOOLTIP_STEP_FEATURES
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
+        runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testMaybeShowPinningEdu_whenTaskbarIsInThreeButtonMode_doesNotShowPinningEdu() {
+        tooltipStep = TOOLTIP_STEP_PINNING
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING)
+        runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testMaybeShowPinningEdu_whenUserHasNotSeen_doesShowPinningEdu() {
+        // Test standalone pinning edu, where user has seen taskbar edu before, but not pinning edu.
+        tooltipStep = TOOLTIP_STEP_PINNING
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING)
+        runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsBeforeTooltipFeaturesStep_whenUserHasNotSeenFeatureEdu_shouldReturnTrue() {
+        tooltipStep = TOOLTIP_STEP_SWIPE
+        assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsBeforeTooltipFeaturesStep_whenUserHasSeenFeatureEdu_shouldReturnFalse() {
+        tooltipStep = TOOLTIP_STEP_NONE
+        assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testHide_whenTooltipIsOpen_shouldCloseTooltip() {
+        tooltipStep = TOOLTIP_STEP_SWIPE
+        assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+        runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue()
+        runOnMainSync { taskbarEduTooltipController.hide() }
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testMaybeShowSearchEdu_whenTaskbarIsTransient_shouldNotShowSearchEdu() {
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+        runOnMainSync { taskbarEduTooltipController.init(taskbarContext.controllers) }
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testMaybeShowSearchEdu_whenTaskbarIsPinnedAndUserHasSeenSearchEdu_shouldNotShowSearchEdu() {
+        searchEduSeen = true
+        assertThat(taskbarEduTooltipController.userHasSeenSearchEdu).isTrue()
+        runOnMainSync { taskbarEduTooltipController.hide() }
+        assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
+        runOnMainSync { taskbarEduTooltipController.init(taskbarContext.controllers) }
+        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 399aea6..4b04dba 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -20,6 +20,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -33,11 +34,13 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
 import com.android.launcher3.logging.StatsLogManager;
 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.Test;
@@ -52,12 +55,16 @@
 
     @Mock
     SystemUiProxy mockSystemUiProxy;
+
+    @Mock
+    ContextualEduStatsManager mockContextualEduStatsManager;
+
     @Mock
     TouchInteractionService mockService;
     @Mock
     Handler mockHandler;
     @Mock
-    AssistUtils mockAssistUtils;
+    ContextualSearchInvoker mockContextualSearchInvoker;
     @Mock
     StatsLogManager mockStatsLogManager;
     @Mock
@@ -100,14 +107,22 @@
                 mockService,
                 mCallbacks,
                 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
+    public void testPressBack_updateContextualEduData() {
+        mNavButtonController.onButtonClick(BUTTON_BACK, mockView);
+        verify(mockContextualEduStatsManager, times(1))
+                .updateEduStats(/* isTrackpad= */ eq(false), eq(GestureType.BACK));
     }
 
     @Test
@@ -151,40 +166,40 @@
     @Test
     public void testLongPressHome_enabled_withoutOverride() {
         mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, times(1)).startAssistant(any());
     }
 
     @Test
     public void testLongPressHome_enabled_withOverride() {
         mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, never()).startAssistant(any());
     }
 
     @Test
     public void testLongPressHome_disabled_withoutOverride() {
         mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, never()).startAssistant(any());
     }
 
     @Test
     public void testLongPressHome_disabled_withOverride() {
         mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, never()).startAssistant(any());
     }
 
@@ -195,12 +210,26 @@
     }
 
     @Test
+    public void testPressHome_updateContextualEduData() {
+        mNavButtonController.onButtonClick(BUTTON_HOME, mockView);
+        verify(mockContextualEduStatsManager, times(1))
+                .updateEduStats(/* isTrackpad= */ eq(false), eq(GestureType.HOME));
+    }
+
+    @Test
     public void testPressRecents() {
         mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
         assertThat(mOverviewToggleCount).isEqualTo(1);
     }
 
     @Test
+    public void testPressRecents_updateContextualEduData() {
+        mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
+        verify(mockContextualEduStatsManager, times(1))
+                .updateEduStats(/* isTrackpad= */ eq(false), eq(GestureType.OVERVIEW));
+    }
+
+    @Test
     public void testPressRecentsWithScreenPinned_noNavigationToOverview() {
         mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
         mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
new file mode 100644
index 0000000..4c94067
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.animation.AnimatorTestRule
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.KeyEvent
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.quickstep.SystemUiProxy
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
+import com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelTablet2023"])
+class TaskbarScrimViewControllerTest {
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+    @get:Rule(order = 1)
+    val context =
+        TaskbarWindowSandboxContext.create { builder ->
+            builder.bindSystemUiProxy(
+                object : SystemUiProxy(this) {
+                    override fun onBackEvent(backEvent: KeyEvent?) {
+                        super.onBackEvent(backEvent)
+                        backPressed = true
+                    }
+                }
+            )
+        }
+    @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 3) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var scrimViewController: TaskbarScrimViewController
+
+    // Default animation duration.
+    private val animationDuration =
+        context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
+
+    private var backPressed = false
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibleChanged_onlyTaskbarVisible_noScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            scrimViewController.updateStateForSysuiFlags(0, true)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarVisibleWithBubblesExpanded_showsScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            animatorTestRule.advanceTimeBy(animationDuration)
+        }
+
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA)
+    }
+
+    @Test
+    @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarHiddenDuringScrim_hidesScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA)
+
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(GONE)
+            animatorTestRule.advanceTimeBy(animationDuration)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarOnHomeHiddenDuringScrim_hidesScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            taskbarUnitTestRule.activityContext.bubbleControllers!!
+                .bubbleStashController
+                .launcherState = BubbleStashController.BubbleLauncherState.HOME
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA)
+
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(GONE)
+            animatorTestRule.advanceTimeBy(animationDuration)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_notificationsOverPinnedTaskbarAndBubbles_noScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(
+                SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE,
+                true,
+            )
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarWithBubbleMenu_darkerScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            scrimViewController.updateStateForSysuiFlags(
+                SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
+                true,
+            )
+        }
+        assertThat(scrimViewController.scrimAlpha).isGreaterThan(BUBBLE_EXPANDED_SCRIM_ALPHA)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testOnTaskbarVisibilityChanged_stashedTaskbarWithBubbles_noScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnClick_scrimShown_performsSystemBack() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimView.isClickable).isTrue()
+
+        getInstrumentation().runOnMainSync { scrimViewController.scrimView.performClick() }
+        assertThat(backPressed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testOnClick_scrimHidden_notClickable() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimView.isClickable).isFalse()
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
new file mode 100644
index 0000000..71f4ef4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
@@ -0,0 +1,680 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.animation.AnimatorTestRule
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_DEVICE_LOCKED
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IME
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_APP_AUTO
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SMALL_SCREEN
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SYSUI
+import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION
+import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION_FOR_IME
+import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
+import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_DURATION
+import com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_STASH
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+@EmulatedDevices(["pixelTablet2023"])
+class TaskbarStashControllerTest {
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+    @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var stashController: TaskbarStashController
+    @InjectController lateinit var viewController: TaskbarViewController
+    @InjectController lateinit var stashedHandleViewController: StashedHandleViewController
+    @InjectController lateinit var dragLayerController: TaskbarDragLayerController
+    @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController
+    @InjectController lateinit var bubbleBarViewController: BubbleBarViewController
+    @InjectController lateinit var bubbleStashController: BubbleStashController
+
+    private val activityContext by taskbarUnitTestRule::activityContext
+
+    // Disable hardware keyboard mode during tests.
+    @Before fun enableSoftwareIme() = TaskbarStashController.enableSoftwareImeForTests(true)
+
+    @After fun resetIme() = TaskbarStashController.enableSoftwareImeForTests(false)
+
+    @After fun cancelTimeoutIfExists() = stashController.cancelTimeoutIfExists()
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testInit_transientMode_stashedInApp() {
+        assertThat(stashController.isStashedInApp).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testInit_pinnedMode_unstashedInApp() {
+        assertThat(stashController.isStashedInApp).isFalse()
+    }
+
+    @Test
+    @UserSetupMode
+    @TaskbarMode(PINNED)
+    fun testInit_userSetupWithPinnedMode_stashedInApp() {
+        assertThat(stashController.isStashedInApp).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSetSetupUiVisible_true_stashedInApp() {
+        getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(true) }
+        assertThat(stashController.isStashedInApp).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSetSetupUiVisible_false_unstashedInApp() {
+        getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(false) }
+        assertThat(stashController.isStashedInApp).isFalse()
+    }
+
+    @Test
+    fun testRecreateAsTransient_timeoutStarted() {
+        var isPinned by TASKBAR_PINNING.asProperty(context)
+        isPinned = true
+        activityContext.controllers.sharedState?.taskbarWasPinned = true
+
+        isPinned = false
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testSupportsVisualStashing_transientMode_supported() {
+        assertThat(stashController.supportsVisualStashing()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSupportsVisualStashing_pinnedMode_supported() {
+        assertThat(stashController.supportsVisualStashing()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testSupportsVisualStashing_threeButtonsMode_unsupported() {
+        assertThat(stashController.supportsVisualStashing()).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetStashDuration_transientMode() {
+        assertThat(stashController.stashDuration).isEqualTo(TRANSIENT_TASKBAR_STASH_DURATION)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetStashDuration_pinnedMode() {
+        assertThat(stashController.stashDuration).isEqualTo(TASKBAR_STASH_DURATION)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsStashed_pinnedInApp_isUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsStashed_transientInApp_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsStashed_transientNotInApp_isUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    fun testIsStashed_stashedInLauncherState_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsStashed_transientInOverview_isUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsStashed_pinnedInOverviewWithIme_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true)
+            stashController.updateStateForFlag(FLAG_STASHED_IME, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsStashed_pinnedTaskbarWithPinnedApp_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, true)
+            stashController.updateStateForFlag(FLAG_STASHED_SYSUI, true) // App pinned.
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    fun testIsInStashedLauncherState_flagUnset_false() {
+        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, false)
+        assertThat(stashController.isInStashedLauncherState).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testIsInStashedLauncherState_flagSetInThreeButtonsMode_false() {
+        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true)
+        assertThat(stashController.isInStashedLauncherState).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsInStashedLauncherState_flagSetInPinnedMode_true() {
+        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true)
+        assertThat(stashController.isInStashedLauncherState).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsTaskbarVisibleAndNotStashing_pinnedButNotVisible_false() {
+        getInstrumentation().runOnMainSync {
+            viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 0f
+        }
+        assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsTaskbarVisibleAndNotStashing_visibleButStashed_false() {
+        getInstrumentation().runOnMainSync {
+            viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f
+        }
+        assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsTaskbarVisibleAndNotStashing_pinnedAndVisible_true() {
+        getInstrumentation().runOnMainSync {
+            viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f
+        }
+        assertThat(stashController.isTaskbarVisibleAndNotStashing).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetTouchableHeight_isStashed_stashedHeight() {
+        assertThat(stashController.touchableHeight).isEqualTo(stashController.stashedHeight)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetTouchableHeight_unstashedTransientMode_heightAndBottomMargin() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false)
+            stashController.applyState(0)
+        }
+
+        val expectedHeight =
+            activityContext.deviceProfile.run { taskbarHeight + taskbarBottomMargin }
+        assertThat(stashController.touchableHeight).isEqualTo(expectedHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetTouchableHeight_pinnedMode_taskbarHeight() {
+        assertThat(stashController.touchableHeight)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetContentHeightToReportToApps_transientMode_stashedHeight() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(stashController.stashedHeight)
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testGetContentHeightToReportToApps_threeButtonsMode_taskbarHeight() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetContentHeightToReportToApps_pinnedMode_taskbarHeight() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    @UserSetupMode
+    fun testGetContentHeightToReportToApps_pinnedInSetupMode_setupWizardInsets() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(context.resources.getDimensionPixelSize(R.dimen.taskbar_suw_insets))
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetContentHeightToReportToApps_pinnedModeButFolded_stashedHeight() {
+        getInstrumentation().runOnMainSync {
+            stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f
+            stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true)
+        }
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(stashController.stashedHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetContentHeightToReportToApps_homeDisabledWhenFolded_zeroHeight() {
+        getInstrumentation().runOnMainSync {
+            stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f
+            stashedHandleViewController.setIsHomeButtonDisabled(true)
+            stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true)
+        }
+        assertThat(stashController.contentHeightToReportToApps).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetTappableHeightToReportToApps_transientMode_zeroHeight() {
+        assertThat(stashController.tappableHeightToReportToApps).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetTappableHeightToReportToApps_pinnedMode_taskbarHeight() {
+        assertThat(stashController.tappableHeightToReportToApps)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbar_updatesState() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_runUnstashAnimation_startsTaskbarTimeout() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_finishTaskbarTimeout_taskbarStashes() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.timeoutAlarm.finishAlarm()
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_autoHideSuspendedForEdu_remainsUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+
+        getInstrumentation().runOnMainSync {
+            autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, true)
+            stashController.updateAndAnimateTransientTaskbar(true)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithBubbles_bubbleBarUnstashes() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.stashBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(false, true)
+        }
+        assertThat(bubbleStashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithoutBubbles_bubbleBarStashed() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.stashBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(false, false)
+        }
+        assertThat(bubbleStashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithBubbles_bubbleBarStashes() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.showBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(true, true)
+        }
+        assertThat(bubbleStashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithoutBubbles_bubbleBarUnstashed() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.showBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(true, false)
+        }
+        assertThat(bubbleStashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_bubbleBarExpandedBeforeTimeout_expandedAfterwards() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleBarViewController.isExpanded = true
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.timeoutAlarm.finishAlarm()
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(bubbleBarViewController.isExpanded).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testToggleTaskbarStash_pinnedMode_doesNothing() {
+        getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testToggleTaskbarStash_transientMode_unstashesTaskbar() {
+        getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testToggleTaskbarStash_twiceInTransientMode_stashesTaskbar() {
+        getInstrumentation().runOnMainSync {
+            stashController.toggleTaskbarStash()
+            stashController.toggleTaskbarStash()
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testToggleTaskbarStash_notInAppWithTransientMode_doesNothing() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.applyState(0)
+            stashController.toggleTaskbarStash()
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testAnimateTransientTaskbar_bubblesShownInOverview_stashesTaskbar() {
+        // Start in Overview. Should unstash Taskbar.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false)
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+
+        // Expand bubbles. Should stash Taskbar.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testAnimatePinnedTaskbar_imeShown_replacesIconsWithHandle() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+        assertThat(viewController.areIconsVisible()).isFalse()
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testAnimatePinnedTaskbar_imeHidden_replacesHandleWithIcons() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(0, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse()
+        assertThat(viewController.areIconsVisible()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testAnimatePinnedTaskbar_imeHidden_verifyAnimationDuration() {
+        // Start with IME shown.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+
+        // Hide IME with animation.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(0, false)
+            // Fast forward without start delay.
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+        // Icons should not be visible yet due to start delay.
+        assertThat(viewController.areIconsVisible()).isFalse()
+
+        // Advance by start delay retroactively. Animation should complete.
+        getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(stashController.taskbarStashStartDelayForIme)
+        }
+        assertThat(viewController.areIconsVisible()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testAnimateThreeButtonsTaskbar_imeShown_hidesIconsAndBg() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+        assertThat(viewController.areIconsVisible()).isFalse()
+        assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testAnimateThreeButtonsTaskbar_imeHidden_showsIconsAndBg() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(0, false)
+            animatorTestRule.advanceTimeBy(
+                TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme
+            )
+        }
+        assertThat(viewController.areIconsVisible()).isTrue()
+        assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(1)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSetSystemGestureInProgress_whileImeShown_unstashesTaskbar() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+
+        getInstrumentation().runOnMainSync {
+            stashController.setSystemGestureInProgress(true)
+            animatorTestRule.advanceTimeBy(
+                TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme
+            )
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testUnlockTransition_pinnedMode_fadesOutHandle() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false)
+            stashController.applyState()
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUnlockTransition_transientMode_fadesOutHandleEarly() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false)
+            stashController.applyState()
+            // Time it takes for just the handle to hide (full stash animation is longer).
+            animatorTestRule.advanceTimeBy(TRANSIENT_TASKBAR_STASH_ALPHA_DURATION)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse()
+    }
+}
+
+private fun TaskbarStashController.updateStateForFlag(flag: Int, value: Boolean) {
+    updateStateForFlag(flag.toLong(), value)
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt
new file mode 100644
index 0000000..b13eafe
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt
@@ -0,0 +1,236 @@
+/*
+ * 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.view.View
+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
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@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 {
+
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var taskbarViewController: TaskbarViewController
+
+    @Test
+    fun testGetPositionInHotseat_allAppsButton_nonRtl() {
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ 6,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ true,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1,
+            )
+        // [>A<] | [HHHHHH]
+        //  -1 -.5  012345
+        assertThat(position).isEqualTo(-1)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_allAppsButton_rtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ true,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1,
+            )
+        // [HHHHHH] | [>A<]
+        //  012345 5.5  6
+        assertThat(position).isEqualTo(numShownHotseatIcons)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_notForRecents_nonRtl() {
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ 6,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1,
+            )
+        // [A] >|< [HHHHHH]
+        // -1  -.5  012345
+        assertThat(position).isEqualTo(-DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_forRecents_nonRtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ true,
+                /* recentTaskIndex = */ -1,
+            )
+        // [A] [HHHHHH] >|< [RR]
+        // -1   012345  5.5  67
+        assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_notForRecents_rtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1,
+            )
+        // [HHHHHH] >|< [A]
+        //  012345  5.5  6
+        assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_forRecents_rtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ true,
+                /* recentTaskIndex = */ -1,
+            )
+        // [HHHHHH][A] >|< [RR]
+        //  012345  6  6.5  78
+        assertThat(position).isEqualTo(numShownHotseatIcons + DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_firstRecentIndex_nonRtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 0
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex,
+            )
+        // [A][HHHHHH] | [>R<R]
+        // -1  012345 5.5  6 7
+        assertThat(position).isEqualTo(numShownHotseatIcons + recentTaskIndex)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_secondRecentIndex_nonRtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 1
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex,
+            )
+        // [A][HHHHHH] | [R>R<]
+        // -1  012345 5.5 6 7
+        assertThat(position).isEqualTo(numShownHotseatIcons + recentTaskIndex)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_firstRecentIndex_rtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 0
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex,
+            )
+        // [HHHHHH][A] | [>R<R]
+        //  012345  6 6.5  7 8
+        assertThat(position).isEqualTo(numShownHotseatIcons + 1 + recentTaskIndex)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_secondRecentIndex_rtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 1
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex,
+            )
+        // [HHHHHH][A] | [R>R<]
+        //  012345  6 6.5 7 8
+        assertThat(position).isEqualTo(numShownHotseatIcons + 1 + recentTaskIndex)
+    }
+}
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 2f0b446..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
@@ -26,6 +26,7 @@
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.notification.NotificationKeyData
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
@@ -43,26 +44,22 @@
 @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
 
     @Test
     fun testToggle_once_showsAllApps() {
-        getInstrumentation().runOnMainSync { allAppsController.toggle() }
+        runOnMainSync { allAppsController.toggle() }
         assertThat(allAppsController.isOpen).isTrue()
     }
 
     @Test
     fun testToggle_twice_closesAllApps() {
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             allAppsController.toggle()
             allAppsController.toggle()
         }
@@ -71,7 +68,7 @@
 
     @Test
     fun testToggle_taskbarRecreated_allAppsReopened() {
-        getInstrumentation().runOnMainSync { allAppsController.toggle() }
+        runOnMainSync { allAppsController.toggle() }
         taskbarUnitTestRule.recreateTaskbar()
         assertThat(allAppsController.isOpen).isTrue()
     }
@@ -138,7 +135,7 @@
 
     @Test
     fun testUpdateNotificationDots_appInfo_hasDot() {
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             allAppsController.setApps(TEST_APPS, 0, emptyMap())
             allAppsController.toggle()
             taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
@@ -162,7 +159,7 @@
 
     @Test
     fun testUpdateNotificationDots_predictedApp_hasDot() {
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
             allAppsController.toggle()
             taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
@@ -185,12 +182,12 @@
 
     @Test
     fun testToggleSearch_searchEditTextFocused() {
-        getInstrumentation().runOnMainSync { allAppsController.toggleSearch() }
-        getInstrumentation().runOnMainSync {
+        runOnMainSync { allAppsController.toggleSearch() }
+        runOnMainSync {
             // All Apps is now attached to window. Open animation is posted but not started.
         }
 
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             // Animation has started. Advance to end of animation.
             animatorTestRule.advanceTimeBy(overlayController.openDuration.toLong())
         }
@@ -198,8 +195,8 @@
         assertThat(editText?.hasFocus()).isTrue()
     }
 
-    private companion object {
-        private val TEST_APPS =
+    companion object {
+        val TEST_APPS =
             Array(16) {
                 AppInfo(
                     ComponentName(
@@ -212,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
new file mode 100644
index 0000000..c8f50f7
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles
+
+import android.view.MotionEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.quickstep.inputconsumers.BubbleBarInputConsumer
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for bubble bar input consumer, namely the static method that indicates whether the input
+ * consumer should handle the event.
+ */
+@RunWith(AndroidJUnit4::class)
+class BubbleBarInputConsumerTest {
+
+    private lateinit var bubbleControllers: BubbleControllers
+
+    @Mock private lateinit var taskbarActivityContext: TaskbarActivityContext
+    @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 bubbleBarSwipeController: BubbleBarSwipeController
+    @Mock private lateinit var bubbleCreator: BubbleCreator
+
+    @Mock private lateinit var motionEvent: MotionEvent
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        bubbleControllers =
+            BubbleControllers(
+                bubbleBarController,
+                bubbleBarViewController,
+                bubbleStashController,
+                Optional.of(bubbleStashedHandleViewController),
+                bubbleDragController,
+                bubbleDismissController,
+                bubbleBarPinController,
+                bubblePinController,
+                Optional.of(bubbleBarSwipeController),
+                bubbleCreator,
+            )
+    }
+
+    @Test
+    fun testIsEventOnBubbles_noTaskbarActivityContext() {
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(null, motionEvent)).isFalse()
+    }
+
+    @Test
+    fun testIsEventOnBubbles_bubblesNotEnabled() {
+        whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(false)
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent))
+            .isFalse()
+    }
+
+    @Test
+    fun testIsEventOnBubbles_noBubbleControllers() {
+        whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true)
+        whenever(taskbarActivityContext.bubbleControllers).thenReturn(null)
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent))
+            .isFalse()
+    }
+
+    @Test
+    fun testIsEventOnBubbles_noBubbles() {
+        whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true)
+        whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers)
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent))
+            .isFalse()
+    }
+
+    @Test
+    fun testIsEventOnBubbles_eventOnStashedHandle() {
+        whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true)
+        whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers)
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        whenever(bubbleStashController.isStashed).thenReturn(true)
+        whenever(bubbleStashedHandleViewController.isEventOverHandle(any())).thenReturn(true)
+
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent))
+            .isTrue()
+    }
+
+    @Test
+    fun testIsEventOnBubbles_eventNotOnStashedHandle() {
+        whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true)
+        whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers)
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        whenever(bubbleStashController.isStashed).thenReturn(true)
+        whenever(bubbleStashedHandleViewController.isEventOverHandle(any())).thenReturn(false)
+
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent))
+            .isFalse()
+    }
+
+    @Test
+    fun testIsEventOnBubbles_eventOnVisibleBubbleView() {
+        whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true)
+        whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers)
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        whenever(bubbleStashController.isStashed).thenReturn(false)
+        whenever(bubbleBarViewController.isBubbleBarVisible).thenReturn(true)
+        whenever(bubbleBarViewController.isEventOverBubbleBar(any())).thenReturn(true)
+
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent))
+            .isTrue()
+    }
+
+    @Test
+    fun testIsEventOnBubbles_eventNotOnVisibleBubbleView() {
+        whenever(taskbarActivityContext.isBubbleBarEnabled).thenReturn(true)
+        whenever(taskbarActivityContext.bubbleControllers).thenReturn(bubbleControllers)
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        whenever(bubbleStashController.isStashed).thenReturn(false)
+        whenever(bubbleBarViewController.isBubbleBarVisible).thenReturn(true)
+        whenever(bubbleBarViewController.isEventOverBubbleBar(any())).thenReturn(false)
+
+        assertThat(BubbleBarInputConsumer.isEventOnBubbles(taskbarActivityContext, motionEvent))
+            .isFalse()
+    }
+}
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
new file mode 100644
index 0000000..4ae8877
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.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.bubbles
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Path
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import androidx.core.graphics.drawable.toBitmap
+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.android.wm.shell.shared.bubbles.BubbleInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleViewTest {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var bubbleView: BubbleView
+    private lateinit var overflowView: BubbleView
+    private lateinit var bubble: BubbleBarBubble
+
+    @Test
+    fun hasUnseenContent_bubble() {
+        setupBubbleViews()
+        assertThat(bubbleView.hasUnseenContent()).isTrue()
+
+        bubbleView.markSeen()
+        assertThat(bubbleView.hasUnseenContent()).isFalse()
+    }
+
+    @Test
+    fun hasUnseenContent_overflow() {
+        setupBubbleViews()
+        assertThat(overflowView.hasUnseenContent()).isFalse()
+    }
+
+    private fun setupBubbleViews() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val inflater = LayoutInflater.from(context)
+
+            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+            overflowView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView
+            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+
+            val bubbleInfo =
+                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(),
+                    "",
+                    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 e9c0dd6..48f3fc2 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
@@ -35,11 +37,16 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
 import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
-import com.android.launcher3.taskbar.bubbles.BubbleStashController
 import com.android.launcher3.taskbar.bubbles.BubbleView
-import com.android.wm.shell.common.bubbles.BubbleInfo
+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 org.junit.Before
 import org.junit.Rule
@@ -63,12 +70,19 @@
     private lateinit var bubbleView: BubbleView
     private lateinit var bubble: BubbleBarBubble
     private lateinit var bubbleBarView: BubbleBarView
+    private lateinit var flyoutContainer: FrameLayout
     private lateinit var bubbleStashController: BubbleStashController
+    private lateinit var flyoutController: BubbleBarFlyoutController
+    private val onExpandedNoOp = Runnable {}
+
+    private val flyoutView: View?
+        get() = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view)
 
     @Before
     fun setUp() {
         animatorScheduler = TestBubbleBarViewAnimatorScheduler()
         PhysicsAnimatorTestUtils.prepareForTest()
+        setupFlyoutController()
     }
 
     @Test
@@ -78,13 +92,19 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // let the animation start and wait for it to complete
@@ -98,12 +118,16 @@
         assertThat(bubbleBarView.scaleX).isEqualTo(1)
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        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)
@@ -111,7 +135,7 @@
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).stashBubbleBarImmediate()
     }
 
@@ -122,13 +146,19 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // let the animation start and wait for it to complete
@@ -142,20 +172,24 @@
         assertThat(bubbleBarView.scaleX).isEqualTo(1)
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
 
         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)
         assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
     }
 
     @Test
@@ -165,13 +199,19 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // wait for the animation to start
@@ -179,7 +219,7 @@
         PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
 
         handleAnimator.assertIsRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
@@ -189,7 +229,7 @@
 
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
 
         // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
@@ -205,23 +245,33 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // let the animation start and wait for it to complete
         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()
@@ -230,7 +280,7 @@
             animator.onStashStateChangingWhileAnimating()
         }
 
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
 
         // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
@@ -246,13 +296,19 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // wait for the animation to start
@@ -260,16 +316,165 @@
         PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
 
         handleAnimator.assertIsRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
         handleAnimator.cancel()
         handleAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         assertThat(animatorScheduler.delayedBlock).isNull()
     }
 
     @Test
+    fun animateBubbleInForStashed_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = true)
+        }
+
+        // wait for the animation to start
+        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).isFalse()
+        assertThat(bubbleBarView.isExpanded).isTrue()
+
+        // verify there is no hide animation
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_expandedWhileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                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.expandedWhileAnimating()
+        }
+
+        // let the animation finish
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(notifiedExpanded).isTrue()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_expandedWhileFullyIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // wait for the animation to end
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // 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)
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(notifiedExpanded).isTrue()
+    }
+
+    @Test
     fun animateToInitialState_inApp() {
         setUpBubbleBar()
         setUpBubbleStashController()
@@ -278,12 +483,18 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = true, isExpanding = false)
@@ -293,18 +504,22 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         barAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         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)
 
         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(0)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(handle.alpha).isEqualTo(1)
@@ -321,12 +536,20 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = true, isExpanding = true)
@@ -336,18 +559,13 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         barAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
 
-        assertThat(animatorScheduler.delayedBlock).isNotNull()
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
-
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
-        assertThat(bubbleBarView.alpha).isEqualTo(1)
-        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-
+        assertThat(animatorScheduler.delayedBlock).isNull()
         verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -360,7 +578,13 @@
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
@@ -370,14 +594,18 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         barAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
+        waitForFlyoutToShow()
+
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        waitForFlyoutToHide()
+
+        assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
@@ -385,6 +613,101 @@
     }
 
     @Test
+    fun animateToInitialState_expandedWhileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+        }
+
+        val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true }
+
+        bubbleBarAnimator.assertIsRunning()
+        assertThat(animator.isAnimating).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the animation finish
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
+    }
+
+    @Test
+    fun animateToInitialState_expandedWhileFullyIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        waitForFlyoutToShow()
+
+        assertThat(animator.isAnimating).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        waitForFlyoutToHide()
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(notifiedExpanded).isTrue()
+    }
+
+    @Test
     fun animateBubbleBarForCollapsed() {
         setUpBubbleBar()
         setUpBubbleStashController()
@@ -394,15 +717,21 @@
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleBarForCollapsed(bubble)
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
         }
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         // verify we started animating
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
 
         // advance the animation handler by the duration of the initial lift
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -414,16 +743,529 @@
         barAnimator.assertIsRunning()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        waitForFlyoutToShow()
+
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        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)
 
         verify(bubbleStashController).showBubbleBarImmediate()
     }
 
+    @Test
+    fun animateBubbleBarForCollapsed_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = true)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(animator.isAnimating).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there is no hide animation
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+        verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
+    }
+
+    @Test
+    fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(animator.isAnimating).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(100)
+        }
+
+        // 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()
+
+        // 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
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+        verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
+    }
+
+    @Test
+    fun animateBubbleBarForCollapsed_expandingWhileFullyIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpanded,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(animator.isAnimating).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there is a pending hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // 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()
+        verify(bubbleStashController).showBubbleBarImmediate()
+        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(100)
+        }
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        // verify the hide animation was rescheduled and run it
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun interruptAnimation_whileAnimatingOut_barToHandle() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("message")
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // interrupt the animation while the bar is animating to the handle
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) {
+            bubbleBarView.alpha < 0.5
+        }
+
+        // we're about to interrupt the animation which will cancel the current animation and start
+        // a new one. pause the scheduler to delay starting the new animation. this allows us to run
+        // the test deterministically
+        animatorScheduler.pauseScheduler = true
+
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there's a new job scheduled and start it. this is starting the animation from the
+        // handle back to the bar
+        assertThat(animatorScheduler.pausedBlock).isNotNull()
+        animatorScheduler.pauseScheduler = false
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.pausedBlock!!)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify the hide animation was rescheduled and run it
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -437,11 +1279,32 @@
             bubbleBarView.addView(overflowView)
 
             val bubbleInfo =
-                BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false)
+                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)
         }
@@ -451,14 +1314,73 @@
     private fun setUpBubbleStashController() {
         bubbleStashController = mock<BubbleStashController>()
         whenever(bubbleStashController.isStashed).thenReturn(true)
-        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
+        whenever(bubbleStashController.getDiffBetweenHandleAndBarCenters())
             .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
-        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
+        whenever(bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation())
             .thenReturn(HANDLE_TRANSLATION)
         whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
             .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)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(ty)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+    }
+
+    private fun waitForFlyoutToShow() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+        assertThat(flyoutView).isNotNull()
+    }
+
+    private fun waitForFlyoutToHide() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+        assertThat(flyoutView).isNull()
+    }
+
+    private fun waitForFlyoutToFadeOutAndBackIn() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(500)
+        }
+        assertThat(flyoutView).isNotNull()
+    }
+
     private fun <T> PhysicsAnimator<T>.assertIsRunning() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             assertThat(isRunning()).isTrue()
@@ -473,20 +1395,30 @@
 
     private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
 
+        var pauseScheduler = false
+        var pausedBlock: Runnable? = null
+            private set
+
         var delayedBlock: Runnable? = null
             private set
 
+        var canceledBlock: Runnable? = null
+            private set
+
         override fun post(block: Runnable) {
+            if (pauseScheduler) {
+                pausedBlock = block
+                return
+            }
             block.run()
         }
 
         override fun postDelayed(delayMillis: Long, block: Runnable) {
-            check(delayedBlock == null) { "there is already a pending block waiting to run" }
             delayedBlock = block
         }
 
         override fun cancel(block: Runnable) {
-            check(delayedBlock == block) { "the pending block does not match the canceled block" }
+            canceledBlock = delayedBlock
             delayedBlock = null
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
new file mode 100644
index 0000000..2997ac9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -0,0 +1,305 @@
+/*
+ * 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.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
+
+/** Unit tests for [BubbleBarFlyoutController] */
+@SmallTest
+@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(icon = null, "sender name", "message")
+    private var onLeft = true
+    private var flyoutTy = 50f
+
+    @Before
+    fun setUp() {
+        flyoutContainer = FrameLayout(context)
+        val positioner =
+            object : BubbleBarFlyoutPositioner {
+                override val isOnLeft
+                    get() = onLeft
+
+                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
+            }
+        flyoutCallbacks = FakeFlyoutCallbacks()
+        val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
+        flyoutController =
+            BubbleBarFlyoutController(flyoutContainer, positioner, flyoutCallbacks, flyoutScheduler)
+    }
+
+    @Test
+    fun flyoutPosition_left() {
+        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
+        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() {
+        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() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutController.hasFlyout()).isTrue()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            flyoutController.collapseFlyout {}
+            animatorTestRule.advanceTimeBy(300)
+        }
+        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(300)
+        }
+        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(300)
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(0)
+    }
+
+    @Test
+    fun collapseFlyout_resetsTopBoundary() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            flyoutController.collapseFlyout {}
+            animatorTestRule.advanceTimeBy(300)
+        }
+        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(300)
+            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(300)
+            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(300)
+        }
+        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(250)
+            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(250)
+            assertThat(flyout.alpha).isEqualTo(1)
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
+    @Test
+    fun updateFlyoutWhileCollapsing() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            animatorTestRule.advanceTimeBy(300)
+        }
+        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 animation ran for 125ms when it was updated, so reversing it should only
+            // run for the same amount of time
+            animatorTestRule.advanceTimeBy(125)
+            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
new file mode 100644
index 0000000..f795ab1
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -0,0 +1,375 @@
+/*
+ * 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 android.animation.AnimatorTestRule
+import android.content.Context
+import android.widget.FrameLayout
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.anim.AnimatedFloat
+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
+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.clearInvocations
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Unit tests for [PersistentBubbleStashController]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PersistentBubbleStashControllerTest {
+
+    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
+    }
+
+    @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+
+    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var bubbleBarView: BubbleBarView
+
+    @Mock lateinit var bubbleBarViewController: BubbleBarViewController
+
+    @Mock lateinit var taskbarInsetsController: TaskbarInsetsController
+
+    private lateinit var persistentTaskBarStashController: PersistentBubbleStashController
+    private lateinit var translationY: AnimatedFloat
+    private lateinit var scale: AnimatedFloat
+    private lateinit var alpha: MultiValueAlpha
+
+    @Before
+    fun setUp() {
+        persistentTaskBarStashController =
+            PersistentBubbleStashController(DefaultDimensionsProvider())
+        setUpBubbleBarView()
+        setUpBubbleBarController()
+        persistentTaskBarStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
+        persistentTaskBarStashController.init(
+            taskbarInsetsController,
+            bubbleBarViewController,
+            null,
+            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.launcherState = BubbleLauncherState.HOME
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch out of the home screen
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
+        }
+
+        // Then translation Y is animating and the bubble bar controller is notified
+        assertThat(translationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+        // Check translation Y is correct and the insets controller is notified
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() {
+        // Given bubble bar has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch to home screen
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+        }
+
+        // Then translation Y is animating and the bubble bar controller is notified
+        assertThat(translationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+
+        // Check translation Y is correct and the insets controller is notified
+        assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y)
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun setBubblesShowingOnOverviewUpdatedToFalse_controllersNotified() {
+        // Given bubble bar is on overview
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW
+        clearInvocations(bubbleBarViewController)
+
+        // When switch out of the overview screen
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
+
+        // Then bubble bar controller is notified
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+    }
+
+    @Test
+    fun setBubblesShowingOnOverviewUpdatedToTrue_controllersNotified() {
+        // When switch to the overview screen
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW
+
+        // Then bubble bar controller is notified
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+    }
+
+    @Test
+    fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
+        // Given screen is locked and bubble bar has bubbles
+        persistentTaskBarStashController.isSysuiLocked = true
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch to the overview screen
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.isSysuiLocked = false
+        }
+
+        // Then
+        assertThat(translationY.isAnimating).isTrue()
+        assertThat(scale.isAnimating).isTrue()
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+
+        // Then bubble bar is fully visible at the correct location
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // Insets controller is notified
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun showBubbleBarImmediateToY() {
+        // Given bubble bar is fully transparent and scaled to 0 at 0 y position
+        val targetY = 341f
+        bubbleBarView.alpha = 0f
+        bubbleBarView.scaleX = 0f
+        bubbleBarView.scaleY = 0f
+        bubbleBarView.translationY = 0f
+
+        // When
+        persistentTaskBarStashController.showBubbleBarImmediate(targetY)
+
+        // Then all property values are updated
+        assertThat(bubbleBarView.translationY).isEqualTo(targetY)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+    }
+
+    @Test
+    fun isTransientTaskbar_false() {
+        assertThat(persistentTaskBarStashController.isTransientTaskBar).isFalse()
+    }
+
+    @Test
+    fun hasHandleView_false() {
+        assertThat(persistentTaskBarStashController.hasHandleView).isFalse()
+    }
+
+    @Test
+    fun isStashed_false() {
+        assertThat(persistentTaskBarStashController.isStashed).isFalse()
+    }
+
+    @Test
+    fun bubbleBarTranslationYForTaskbar() {
+        // Give bubble bar is on home
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        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.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) }
+    }
+
+    private fun setUpBubbleBarView() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarView = BubbleBarView(context)
+            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+        }
+    }
+
+    private fun setUpBubbleBarController() {
+        translationY = AnimatedFloat(Runnable { bubbleBarView.translationY = translationY.value })
+        scale =
+            AnimatedFloat(
+                Runnable {
+                    val scale: Float = scale.value
+                    bubbleBarView.scaleX = scale
+                    bubbleBarView.scaleY = scale
+                }
+            )
+        alpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */)
+
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(translationY)
+        whenever(bubbleBarViewController.bubbleBarScaleY).thenReturn(scale)
+        whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(alpha)
+        whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT)
+    }
+}
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
new file mode 100644
index 0000000..96c2f45
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.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.launcher3.taskbar.bubbles.stashing
+
+class ImmediateAction : BubbleStashController.ControllersAfterInitAction {
+    override fun runAfterInit(action: Runnable) = action.run()
+}
+
+class DefaultDimensionsProvider(
+    private val taskBarBottomSpace: Int = TASKBAR_BOTTOM_SPACE,
+    private val taskBarHeight: Int = TASKBAR_HEIGHT,
+) : BubbleStashController.TaskbarHotseatDimensionsProvider {
+    override fun getTaskbarBottomSpace(): Int = taskBarBottomSpace
+
+    override fun getTaskbarHeight(): Int = taskBarHeight
+
+    companion object {
+        const val TASKBAR_BOTTOM_SPACE = 0
+        const val TASKBAR_HEIGHT = 110
+    }
+}
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
new file mode 100644
index 0000000..1bbd12a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -0,0 +1,532 @@
+/*
+ * 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 android.animation.AnimatorSet
+import android.animation.AnimatorTestRule
+import android.content.Context
+import android.view.View
+import android.widget.FrameLayout
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+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
+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.atLeastOnce
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Unit tests for [TransientBubbleStashController]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TransientBubbleStashControllerTest {
+
+    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.toFloat()
+        const val HANDLE_VIEW_WIDTH = 150
+        const val HANDLE_VIEW_HEIGHT = 4
+        const val BUBBLE_BAR_STASHED_TRANSLATION_Y = -4.5f
+    }
+
+    @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+
+    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController
+
+    @Mock lateinit var bubbleBarViewController: BubbleBarViewController
+
+    @Mock lateinit var taskbarInsetsController: TaskbarInsetsController
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var bubbleBarView: BubbleBarView
+    private lateinit var stashedHandleView: StashedHandleView
+    private lateinit var bubbleView: BubbleView
+    private lateinit var barTranslationY: AnimatedFloat
+    private lateinit var barScaleX: AnimatedFloat
+    private lateinit var barScaleY: AnimatedFloat
+    private lateinit var barAlpha: MultiValueAlpha
+    private lateinit var bubbleOffsetY: AnimatedFloat
+    private lateinit var bubbleAlpha: AnimatedFloat
+    private lateinit var backgroundAlpha: AnimatedFloat
+    private lateinit var stashedHandleAlpha: MultiValueAlpha
+    private lateinit var stashedHandleScale: AnimatedFloat
+    private lateinit var stashedHandleTranslationY: AnimatedFloat
+    private lateinit var stashPhysicsAnimator: PhysicsAnimator<View>
+
+    private lateinit var mTransientBubbleStashController: TransientBubbleStashController
+
+    @Before
+    fun setUp() {
+        val taskbarHotseatDimensionsProvider =
+            DefaultDimensionsProvider(taskBarBottomSpace = TASKBAR_BOTTOM_SPACE)
+        mTransientBubbleStashController =
+            TransientBubbleStashController(taskbarHotseatDimensionsProvider, context)
+        setUpBubbleBarView()
+        setUpBubbleBarController()
+        setUpStashedHandleView()
+        setUpBubbleStashedHandleViewController()
+        PhysicsAnimatorTestUtils.prepareForTest()
+        mTransientBubbleStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
+        mTransientBubbleStashController.init(
+            taskbarInsetsController,
+            bubbleBarViewController,
+            bubbleStashedHandleViewController,
+            ImmediateAction(),
+        )
+    }
+
+    @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.launcherState = BubbleLauncherState.HOME
+        }
+
+        // Then BubbleBarView is animating, BubbleBarViewController controller is notified
+        assertThat(barTranslationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+        // Then translation Y is correct and the insets controller is notified
+        assertThat(barTranslationY.isAnimating).isFalse()
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y)
+    }
+
+    @Test
+    fun setBubblesShowingOnOverviewUpdatedToTrue_barPositionYUpdated_controllersNotified() {
+        // Given bubble bar is on overview and has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch out of the home screen
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW
+        }
+
+        // Then BubbleBarView is animating, BubbleBarViewController controller is notified
+        assertThat(barTranslationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+        // Then translation Y is correct and the insets controller is notified
+        assertThat(barTranslationY.isAnimating).isFalse()
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+    }
+
+    @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
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        val bubbleInitialTranslation = bubbleView.translationY
+
+        // When stash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = true,
+                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 stashed
+        assertThat(mTransientBubbleStashController.isStashed).isTrue()
+        assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(0f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX())
+        assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY())
+        assertThat(bubbleBarView.background.alpha).isEqualTo(255)
+        // Handle view is visible
+        assertThat(stashedHandleView.translationY).isEqualTo(0)
+        assertThat(stashedHandleView.alpha).isEqualTo(1)
+        // Bubble view is reset
+        assertThat(bubbleView.translationY).isEqualTo(bubbleInitialTranslation)
+        assertThat(bubbleView.alpha).isEqualTo(1f)
+    }
+
+    @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.launcherState = BubbleLauncherState.OVERVIEW
+            whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        }
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+
+        // When switch to the overview screen
+        getInstrumentation().runOnMainSync { mTransientBubbleStashController.isSysuiLocked = false }
+
+        // Then
+        assertThat(barTranslationY.isAnimating).isTrue()
+        assertThat(barScaleX.isAnimating).isTrue()
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+
+        // Then bubble bar is fully visible at the correct location
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        assertThat(bubbleBarView.translationY)
+            .isEqualTo(PersistentBubbleStashControllerTest.TASK_BAR_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // Insets controller is notified
+        verify(taskbarInsetsController, atLeastOnce())
+            .onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun showBubbleBarImmediateToY() {
+        // Given bubble bar is fully transparent and scaled to 0 at 0 y position
+        val targetY = 341f
+        bubbleBarView.alpha = 0f
+        bubbleBarView.scaleX = 0f
+        bubbleBarView.scaleY = 0f
+        bubbleBarView.translationY = 0f
+        stashedHandleView.translationY = targetY
+
+        // When
+        mTransientBubbleStashController.showBubbleBarImmediate(targetY)
+
+        // Then all property values are updated
+        assertThat(bubbleBarView.translationY).isEqualTo(targetY)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        // Handle is transparent
+        assertThat(stashedHandleView.alpha).isEqualTo(0)
+        // Insets controller is notified
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        // Bubble bar visibility updated
+        verify(bubbleBarViewController).setHiddenForStashed(false)
+    }
+
+    @Test
+    fun stashBubbleBarImmediate() {
+        // When
+        mTransientBubbleStashController.stashBubbleBarImmediate()
+
+        // Then all property values are updated
+        assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX())
+        assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY())
+        // Handle is visible at correct Y position
+        assertThat(stashedHandleView.alpha).isEqualTo(1)
+        assertThat(stashedHandleView.translationY).isEqualTo(0)
+        // Insets controller is notified
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        // Bubble bar visibility updated
+        verify(bubbleBarViewController).setHiddenForStashed(true)
+    }
+
+    @Test
+    fun getTouchableHeight_stashed_stashHeightReturned() {
+        // When
+        mTransientBubbleStashController.isStashed = true
+        val height = mTransientBubbleStashController.getTouchableHeight()
+
+        // Then
+        assertThat(height).isEqualTo(HANDLE_VIEW_HEIGHT)
+    }
+
+    @Test
+    fun getTouchableHeight_unstashed_barHeightReturned() {
+        // When BubbleBar is not stashed
+        mTransientBubbleStashController.isStashed = false
+        val height = mTransientBubbleStashController.getTouchableHeight()
+
+        // Then bubble bar height is returned
+        assertThat(height).isEqualTo(BUBBLE_BAR_HEIGHT)
+    }
+
+    private fun advanceTimeBy(advanceMs: Long) {
+        // Advance animator for on-device tests
+        getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) }
+    }
+
+    private fun setUpBubbleBarView() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarView = BubbleBarView(context)
+            bubbleBarView.layoutParams =
+                FrameLayout.LayoutParams(BUBBLE_BAR_WIDTH, BUBBLE_BAR_HEIGHT)
+            bubbleView = BubbleView(context)
+            bubbleBarView.addBubble(bubbleView)
+            bubbleBarView.layout(0, 0, BUBBLE_BAR_WIDTH, BUBBLE_BAR_HEIGHT)
+        }
+    }
+
+    private fun setUpStashedHandleView() {
+        getInstrumentation().runOnMainSync {
+            stashedHandleView = StashedHandleView(context)
+            stashedHandleView.layoutParams =
+                FrameLayout.LayoutParams(HANDLE_VIEW_WIDTH, HANDLE_VIEW_HEIGHT)
+        }
+    }
+
+    private fun setUpBubbleBarController() {
+        barTranslationY =
+            AnimatedFloat(Runnable { bubbleBarView.translationY = barTranslationY.value })
+        bubbleOffsetY = AnimatedFloat { value -> bubbleBarView.setBubbleOffsetY(value) }
+        barScaleX = AnimatedFloat { value -> bubbleBarView.scaleX = value }
+        barScaleY = AnimatedFloat { value -> bubbleBarView.scaleY = value }
+        barAlpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */)
+        bubbleAlpha = AnimatedFloat { value -> bubbleBarView.setBubbleAlpha(value) }
+        backgroundAlpha = AnimatedFloat { value -> bubbleBarView.setBackgroundAlpha(value) }
+
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(barTranslationY)
+        whenever(bubbleBarViewController.bubbleOffsetY).thenReturn(bubbleOffsetY)
+        whenever(bubbleBarViewController.bubbleBarBackgroundScaleX).thenReturn(barScaleX)
+        whenever(bubbleBarViewController.bubbleBarBackgroundScaleY).thenReturn(barScaleY)
+        whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(barAlpha)
+        whenever(bubbleBarViewController.bubbleBarBubbleAlpha).thenReturn(bubbleAlpha)
+        whenever(bubbleBarViewController.bubbleBarBackgroundAlpha).thenReturn(backgroundAlpha)
+        whenever(bubbleBarViewController.bubbleBarCollapsedWidth)
+            .thenReturn(BUBBLE_BAR_WIDTH.toFloat())
+        whenever(bubbleBarViewController.bubbleBarCollapsedHeight)
+            .thenReturn(BUBBLE_BAR_HEIGHT.toFloat())
+        whenever(bubbleBarViewController.createRevealAnimatorForStashChange(any()))
+            .thenReturn(AnimatorSet())
+    }
+
+    private fun setUpBubbleStashedHandleViewController() {
+        stashedHandleTranslationY =
+            AnimatedFloat(Runnable { stashedHandleView.translationY = barTranslationY.value })
+        stashedHandleScale =
+            AnimatedFloat(
+                Runnable {
+                    val scale: Float = barScaleX.value
+                    bubbleBarView.scaleX = scale
+                    bubbleBarView.scaleY = scale
+                }
+            )
+        stashedHandleAlpha = MultiValueAlpha(stashedHandleView, 1 /* num alpha channels */)
+        stashPhysicsAnimator = PhysicsAnimator.getInstance(stashedHandleView)
+        whenever(bubbleStashedHandleViewController.stashedHandleAlpha)
+            .thenReturn(stashedHandleAlpha)
+        whenever(bubbleStashedHandleViewController.physicsAnimator).thenReturn(stashPhysicsAnimator)
+        whenever(bubbleStashedHandleViewController.stashedWidth).thenReturn(HANDLE_VIEW_WIDTH)
+        whenever(bubbleStashedHandleViewController.stashedHeight).thenReturn(HANDLE_VIEW_HEIGHT)
+        whenever(bubbleStashedHandleViewController.setTranslationYForSwipe(any())).thenAnswer {
+            invocation ->
+            (invocation.arguments[0] as Float).also { stashedHandleView.translationY = it }
+        }
+    }
+}
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 f946d4d..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,13 +18,13 @@
 
 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
 import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY
 import com.android.launcher3.AbstractFloatingView.hasOpenView
 import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
@@ -41,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
@@ -64,7 +60,7 @@
     @Test
     fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() {
         val context1 = getOnUiThread { overlayController.requestWindow() }
-        getInstrumentation().runOnMainSync { overlayController.hideWindow() }
+        runOnMainSync { overlayController.hideWindow() }
 
         val context2 = getOnUiThread { overlayController.requestWindow() }
         assertThat(context1).isNotSameInstanceAs(context2)
@@ -73,7 +69,7 @@
     @Test
     fun testRequestWindow_afterHidingOverlay_createsNewWindow() {
         val context1 = getOnUiThread { overlayController.requestWindow() }
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             TestOverlayView.show(context1)
             overlayController.hideWindow()
         }
@@ -84,16 +80,14 @@
 
     @Test
     fun testRequestWindow_addsProxyView() {
-        getInstrumentation().runOnMainSync {
-            TestOverlayView.show(overlayController.requestWindow())
-        }
+        runOnMainSync { TestOverlayView.show(overlayController.requestWindow()) }
         assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
     }
 
     @Test
     fun testRequestWindow_closeProxyView_closesOverlay() {
         val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)
         }
         assertThat(overlay.isOpen).isFalse()
@@ -103,13 +97,13 @@
     fun testRequestWindow_attachesDragLayer() {
         val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
         // Allow drag layer to attach before checking.
-        getInstrumentation().runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() }
+        runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() }
     }
 
     @Test
     fun testHideWindow_closesOverlay() {
         val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
-        getInstrumentation().runOnMainSync { overlayController.hideWindow() }
+        runOnMainSync { overlayController.hideWindow() }
         assertThat(overlay.isOpen).isFalse()
     }
 
@@ -118,7 +112,7 @@
         val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
 
         // Wait for drag layer to be attached to window before hiding.
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             overlayController.hideWindow()
             assertThat(dragLayer.isAttachedToWindow).isFalse()
         }
@@ -132,7 +126,7 @@
                 Pair(TestOverlayView.show(context), TestOverlayView.show(context))
             }
 
-        getInstrumentation().runOnMainSync { overlay1.close(false) }
+        runOnMainSync { overlay1.close(false) }
         assertThat(overlay2.isOpen).isTrue()
         assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
     }
@@ -145,7 +139,7 @@
                 Pair(TestOverlayView.show(context), TestOverlayView.show(context))
             }
 
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             overlay1.close(false)
             overlay2.close(false)
         }
@@ -154,9 +148,7 @@
 
     @Test
     fun testRecreateTaskbar_closesWindow() {
-        getInstrumentation().runOnMainSync {
-            TestOverlayView.show(overlayController.requestWindow())
-        }
+        runOnMainSync { TestOverlayView.show(overlayController.requestWindow()) }
         taskbarUnitTestRule.recreateTaskbar()
         assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse()
     }
@@ -166,29 +158,25 @@
         val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
         TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo())
         // Make sure TaskStackChangeListeners' Handler posts the callback before checking state.
-        getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() }
+        runOnMainSync { assertThat(overlay.isOpen).isFalse() }
     }
 
     @Test
     fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() {
         val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
-        getInstrumentation().runOnMainSync {
-            taskbarContext.controllers.sharedState?.allAppsVisible = false
-        }
+        runOnMainSync { taskbarContext.controllers.sharedState?.allAppsVisible = false }
 
         TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged()
-        getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isTrue() }
+        runOnMainSync { assertThat(overlay.isOpen).isTrue() }
     }
 
     @Test
     fun testTaskStackChanged_allAppsOpen_closesOverlay() {
         val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
-        getInstrumentation().runOnMainSync {
-            taskbarContext.controllers.sharedState?.allAppsVisible = true
-        }
+        runOnMainSync { taskbarContext.controllers.sharedState?.allAppsVisible = true }
 
         TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged()
-        getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() }
+        runOnMainSync { assertThat(overlay.isOpen).isFalse() }
     }
 
     @Test
@@ -198,7 +186,7 @@
             TestOverlayView.show(context).apply { type = TYPE_OPTIONS_POPUP }
         }
 
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             overlayController.updateLauncherDeviceProfile(
                 overlayController.launcherDeviceProfile
                     .toBuilder(context)
@@ -217,7 +205,7 @@
             TestOverlayView.show(context).apply { type = TYPE_TASKBAR_ALL_APPS }
         }
 
-        getInstrumentation().runOnMainSync {
+        runOnMainSync {
             overlayController.updateLauncherDeviceProfile(
                 overlayController.launcherDeviceProfile
                     .toBuilder(context)
@@ -230,9 +218,8 @@
     }
 
     private class TestOverlayView
-    private constructor(
-        private val overlayContext: TaskbarOverlayContext,
-    ) : AbstractFloatingView(overlayContext, null) {
+    private constructor(private val overlayContext: TaskbarOverlayContext) :
+        AbstractFloatingView(overlayContext, null) {
 
         var type = TYPE_OPTIONS_POPUP
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index c48947e..74b154a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -61,7 +61,7 @@
                 val mode = taskbarMode.mode
 
                 getInstrumentation().runOnMainSync {
-                    context.applicationContext.putObject(
+                    context.putObject(
                         DisplayController.INSTANCE,
                         object : DisplayController(context) {
                             override fun getInfo(): Info {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
index f75e542..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
@@ -24,6 +23,7 @@
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 import com.android.launcher3.util.NavigationMode
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -31,11 +31,11 @@
 import org.junit.runner.RunWith
 
 @RunWith(LauncherMultivalentJUnit::class)
+@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 60c8dec..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt
+++ /dev/null
@@ -1,71 +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 com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
-import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE
-import kotlin.reflect.KProperty
-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 prefs = LauncherPrefs.get(context)
-
-    var isPinned by PinningPreference(TASKBAR_PINNING)
-    var isPinnedInDesktopMode by PinningPreference(TASKBAR_PINNING_IN_DESKTOP_MODE)
-
-    override fun apply(base: Statement, description: Description): Statement {
-        return object : Statement() {
-            override fun evaluate() {
-                val wasPinned = isPinned
-                val wasPinnedInDesktopMode = isPinnedInDesktopMode
-                try {
-                    base.evaluate()
-                } finally {
-                    isPinned = wasPinned
-                    isPinnedInDesktopMode = wasPinnedInDesktopMode
-                }
-            }
-        }
-    }
-
-    private inner class PinningPreference(private val constantItem: ConstantItem<Boolean>) {
-        operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
-            return prefs.get(constantItem)
-        }
-
-        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
-            getInstrumentation().runOnMainSync { prefs.put(constantItem, value) }
-        }
-    }
-}
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 3f0a238..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
+++ /dev/null
@@ -1,98 +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.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.Description
-import org.junit.runner.RunWith
-import org.junit.runners.model.Statement
-
-@RunWith(LauncherMultivalentJUnit::class)
-class TaskbarPinningPreferenceRuleTest {
-    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
-
-    private val preferenceRule = TaskbarPinningPreferenceRule(context)
-
-    @Test
-    fun testEnablePinning_verifyDisplayController() {
-        onSetup {
-            preferenceRule.isPinned = true
-            preferenceRule.isPinnedInDesktopMode = false
-            assertThat(DisplayController.isPinnedTaskbar(context)).isTrue()
-        }
-    }
-
-    @Test
-    fun testDisablePinning_verifyDisplayController() {
-        onSetup {
-            preferenceRule.isPinned = false
-            preferenceRule.isPinnedInDesktopMode = false
-            assertThat(DisplayController.isPinnedTaskbar(context)).isFalse()
-        }
-    }
-
-    @Test
-    fun testEnableDesktopPinning_verifyDisplayController() {
-        onSetup {
-            preferenceRule.isPinned = false
-            preferenceRule.isPinnedInDesktopMode = true
-            assertThat(DisplayController.isPinnedTaskbar(context)).isTrue()
-        }
-    }
-
-    @Test
-    fun testDisableDesktopPinning_verifyDisplayController() {
-        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,
-        )
-    }
-
-    private companion object {
-        private val DESCRIPTION =
-            Description.createSuiteDescription(TaskbarPinningPreferenceRule::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 a966d2a..096f879 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -19,26 +19,26 @@
 import android.app.Instrumentation
 import android.app.PendingIntent
 import android.content.IIntentSender
-import android.content.Intent
-import android.provider.Settings
 import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE
 import android.provider.Settings.Secure.USER_SETUP_COMPLETE
+import android.provider.Settings.Secure.getUriFor
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.ServiceTestRule
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarControllers
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
+import com.android.launcher3.taskbar.TaskbarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleControllers
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
-import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
-import com.android.launcher3.util.ModelTestExtensions.loadModelSync
 import com.android.launcher3.util.TestUtil
 import com.android.quickstep.AllAppsActionManager
-import com.android.quickstep.TouchInteractionService
-import com.android.quickstep.TouchInteractionService.TISBinder
+import java.lang.reflect.Field
+import java.lang.reflect.ParameterizedType
+import java.util.Optional
 import org.junit.Assume.assumeTrue
-import org.junit.rules.RuleChain
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -75,11 +75,6 @@
 ) : TestRule {
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
-    private val serviceTestRule = ServiceTestRule()
-
-    private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE)
-    private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE)
-    private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule)
 
     private lateinit var taskbarManager: TaskbarManager
 
@@ -90,10 +85,6 @@
         }
 
     override fun apply(base: Statement, description: Description): Statement {
-        return settingRules.apply(createStatement(base, description), description)
-    }
-
-    private fun createStatement(base: Statement, description: Description): Statement {
         return object : Statement() {
             override fun evaluate() {
 
@@ -105,34 +96,10 @@
                 }
 
                 // Process secure setting annotations.
-                instrumentation.runOnMainSync {
-                    userSetupCompleteRule.putInt(
-                        if (description.getAnnotation(UserSetupMode::class.java) != null) {
-                            0
-                        } else {
-                            1
-                        }
-                    )
-                    kidsModeRule.putInt(
-                        if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
-                    )
-                }
-
-                // Check for existing Taskbar instance from Launcher process.
-                val launcherTaskbarManager: TaskbarManager? =
-                    if (!isRunningInRobolectric) {
-                        try {
-                            val tisBinder =
-                                serviceTestRule.bindService(
-                                    Intent(context, TouchInteractionService::class.java)
-                                ) as? TISBinder
-                            tisBinder?.taskbarManager
-                        } catch (_: Exception) {
-                            null
-                        }
-                    } else {
-                        null
-                    }
+                context.settingsCacheSandbox[getUriFor(USER_SETUP_COMPLETE)] =
+                    if (description.getAnnotation(UserSetupMode::class.java) != null) 0 else 1
+                context.settingsCacheSandbox[getUriFor(NAV_BAR_KIDS_MODE)] =
+                    if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
 
                 taskbarManager =
                     TestUtil.getOnUiThread {
@@ -143,6 +110,7 @@
                                     PendingIntent(IIntentSender.Default())
                                 },
                                 object : TaskbarNavButtonCallbacks {},
+                                DesktopVisibilityController(context),
                             ) {
                             override fun recreateTaskbar() {
                                 super.recreateTaskbar()
@@ -152,21 +120,15 @@
                     }
 
                 try {
-                    LauncherAppState.getInstance(context).model.loadModelSync()
+                    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)
                 }
             }
         }
@@ -176,19 +138,38 @@
     fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbar() }
 
     private fun injectControllers() {
-        val controllers = activityContext.controllers
-        val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
+        val bubbleControllerTypes =
+            BubbleControllers::class.java.fields.map { f ->
+                if (f.type == Optional::class.java) {
+                    (f.genericType as ParameterizedType).actualTypeArguments[0] as Class<*>
+                } else {
+                    f.type
+                }
+            }
         testInstance.javaClass.fields
             .filter { it.isAnnotationPresent(InjectController::class.java) }
             .forEach {
-                it.set(
-                    testInstance,
-                    controllerFieldsByType[it.type]?.get(controllers)
-                        ?: throw NoSuchElementException("Failed to find controller for ${it.type}"),
-                )
+                val controllers: Any =
+                    if (it.type in bubbleControllerTypes) {
+                        activityContext.controllers.bubbleControllers.orElseThrow {
+                            NoSuchElementException("Bubble controllers are not initialized")
+                        }
+                    } else {
+                        activityContext.controllers
+                    }
+                injectController(it, testInstance, controllers)
             }
     }
 
+    private fun injectController(field: Field, testInstance: Any, controllers: Any) {
+        val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
+        field.set(
+            testInstance,
+            controllerFieldsByType[field.type]?.get(controllers)
+                ?: throw NoSuchElementException("Failed to find controller for ${field.type}"),
+        )
+    }
+
     /**
      * Annotates test controller fields to inject the corresponding controllers from the current
      * [TaskbarControllers] instance.
@@ -210,25 +191,4 @@
     @Retention(AnnotationRetention.RUNTIME)
     @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
     annotation class NavBarKidsMode
-
-    /** Rule for Taskbar integer-based secure settings. */
-    private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule {
-
-        override fun apply(base: Statement, description: Description): Statement {
-            return object : Statement() {
-                override fun evaluate() {
-                    val originalValue =
-                        Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0)
-                    try {
-                        base.evaluate()
-                    } finally {
-                        instrumentation.runOnMainSync { putInt(originalValue) }
-                    }
-                }
-            }
-        }
-
-        /** Puts [value] into secure settings under [settingName]. */
-        fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value)
-    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
index 234e499..7daa142 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -16,28 +16,34 @@
 
 package com.android.launcher3.taskbar.rules
 
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.TaskbarKeyguardController
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.taskbar.TaskbarStashController
+import com.android.launcher3.taskbar.bubbles.BubbleBarController
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.wm.shell.Flags
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.Description
 import org.junit.runner.RunWith
 import org.junit.runners.model.Statement
 
 @RunWith(LauncherMultivalentJUnit::class)
-@EmulatedDevices(["pixelFoldable2023"])
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarUnitTestRuleTest {
 
-    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val setFlagsRule = SetFlagsRule()
 
     @Test
     fun testSetup_taskbarInitialized() {
@@ -127,6 +133,44 @@
         }
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    fun testInjectBubbleController_bubbleFlagOn_isInjected() {
+        val testClass =
+            object {
+                @InjectController lateinit var controller: BubbleBarController
+                val isInjected: Boolean
+                    get() = ::controller.isInitialized
+            }
+
+        TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+
+        onSetup(TaskbarUnitTestRule(testClass, context)) {
+            assertThat(testClass.isInjected).isTrue()
+        }
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    fun testInjectBubbleController_bubbleFlagOff_exceptionThrown() {
+        val testClass =
+            object {
+                @InjectController lateinit var controller: BubbleBarController
+            }
+
+        // We cannot use #assertThrows because we also catch an assumption violated exception when
+        // running #evaluate on devices that do not support Taskbar.
+        val result =
+            try {
+                TaskbarUnitTestRule(testClass, context)
+                    .apply(EMPTY_STATEMENT, DESCRIPTION)
+                    .evaluate()
+            } catch (e: NoSuchElementException) {
+                e
+            }
+        assertThat(result).isInstanceOf(NoSuchElementException::class.java)
+    }
+
     @Test
     fun testUserSetupMode_default_isComplete() {
         onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index 321e7a9..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() = 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 ad4b4de..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,29 +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)
+@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
new file mode 100644
index 0000000..dc5223c
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -0,0 +1,338 @@
+/*
+ * 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.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED;
+
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.SystemClock;
+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.StatefulContainer;
+import com.android.launcher3.util.SystemUiController;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.util.ContextInitListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.system.InputConsumerController;
+
+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;
+
+import java.util.Collections;
+import java.util.HashMap;
+
+public abstract class AbsSwipeUpHandlerTestCase<
+        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 RecentsAnimationDeviceState mRecentsAnimationDeviceState =
+            new RecentsAnimationDeviceState(mContext, true);
+    protected final InputConsumerController mInputConsumerController =
+            InputConsumerController.getRecentsAnimationInputConsumer();
+    protected final ActivityManager.RunningTaskInfo mRunningTaskInfo =
+            new ActivityManager.RunningTaskInfo();
+    protected final TopTaskTracker.CachedTaskInfo mCachedTaskInfo =
+            new TopTaskTracker.CachedTaskInfo(Collections.singletonList(mRunningTaskInfo));
+    protected final RemoteAnimationTarget mRemoteAnimationTarget = new RemoteAnimationTarget(
+            /* taskId= */ 0,
+            /* mode= */ RemoteAnimationTarget.MODE_CLOSING,
+            /* leash= */ new SurfaceControl(),
+            /* isTranslucent= */ false,
+            /* clipRect= */ null,
+            /* contentInsets= */ null,
+            /* prefixOrderIndex= */ 0,
+            /* position= */ null,
+            /* localBounds= */ null,
+            /* screenSpaceBounds= */ null,
+            new Configuration().windowConfiguration,
+            /* isNotInRecents= */ false,
+            /* startLeash= */ null,
+            /* 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());
+
+    protected TaskAnimationManager mTaskAnimationManager;
+
+    @Mock protected CONTAINER_INTERFACE mActivityInterface;
+    @Mock protected ContextInitListener<?> mContextInitListener;
+    @Mock protected RecentsAnimationController mRecentsAnimationController;
+    @Mock protected STATE_TYPE mState;
+    @Mock protected ViewTreeObserver mViewTreeObserver;
+    @Mock protected DragLayer mDragLayer;
+    @Mock protected LauncherRootView mRootView;
+    @Mock protected SystemUiController mSystemUiController;
+    @Mock protected GestureState mGestureState;
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Before
+    public void setUpRunningTaskInfo() {
+        mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    }
+
+    @Before
+    public void setUpGestureState() {
+        when(mGestureState.getRunningTask()).thenReturn(mCachedTaskInfo);
+        when(mGestureState.getLastAppearedTaskIds()).thenReturn(new int[0]);
+        when(mGestureState.getLastStartedTaskIds()).thenReturn(new int[1]);
+        when(mGestureState.getHomeIntent()).thenReturn(new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+        doReturn(mActivityInterface).when(mGestureState).getContainerInterface();
+    }
+
+    @Before
+    public void setUpRecentsView() {
+        RECENTS_VIEW recentsView = getRecentsView();
+        when(recentsView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
+        doAnswer(answer -> {
+            runOnMainSync(() -> answer.<Runnable>getArgument(0).run());
+            return this;
+        }).when(recentsView).runOnPageScrollsInitialized(any());
+    }
+
+    @Before
+    public void setUpRecentsContainer() {
+        mTaskAnimationManager = new TaskAnimationManager(mContext, getRecentsWindowManager());
+        RecentsViewContainer recentsContainer = getRecentsContainer();
+        RECENTS_VIEW recentsView = getRecentsView();
+
+        when(recentsContainer.getDeviceProfile()).thenReturn(new DeviceProfile());
+        when(recentsContainer.getOverviewPanel()).thenReturn(recentsView);
+        when(recentsContainer.getDragLayer()).thenReturn(mDragLayer);
+        when(recentsContainer.getRootView()).thenReturn(mRootView);
+        when(recentsContainer.getSystemUiController()).thenReturn(mSystemUiController);
+        when(mActivityInterface.createActivityInitListener(any()))
+                .thenReturn(mContextInitListener);
+        doReturn(recentsContainer).when(mActivityInterface).getCreatedContainer();
+        doAnswer(answer -> {
+            answer.<Runnable>getArgument(0).run();
+            return this;
+        }).when(recentsContainer).runOnBindToTouchInteractionService(any());
+    }
+
+    @Test
+    public void testInitWhenReady_registersActivityInitListener() {
+        String reasonString = "because i said so";
+
+        createSwipeHandler().initWhenReady(reasonString);
+        verify(mContextInitListener).register(eq(reasonString));
+    }
+
+    @Test
+    public void testOnRecentsAnimationCanceled_unregistersActivityInitListener() {
+        createSwipeHandler()
+                .onRecentsAnimationCanceled(new HashMap<>());
+
+        runOnMainSync(() -> verify(mContextInitListener)
+                .unregister(eq("AbsSwipeUpHandler.onRecentsAnimationCanceled")));
+    }
+
+    @Test
+    public void testOnConsumerAboutToBeSwitched_unregistersActivityInitListener() {
+        createSwipeHandler().onConsumerAboutToBeSwitched();
+
+        runOnMainSync(() -> verify(mContextInitListener)
+                .unregister("AbsSwipeUpHandler.invalidateHandler"));
+    }
+
+    @Test
+    public void testOnConsumerAboutToBeSwitched_midQuickSwitch_unregistersActivityInitListener() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.NEW_TASK)
+                .onConsumerAboutToBeSwitched();
+
+        runOnMainSync(() -> verify(mContextInitListener)
+                .unregister(eq("AbsSwipeUpHandler.cancelCurrentAnimation")));
+    }
+
+    @Test
+    public void testStartNewTask_finishesRecentsAnimationController() {
+        SWIPE_HANDLER absSwipeUpHandler = createSwipeHandler();
+
+        onRecentsAnimationStart(absSwipeUpHandler);
+
+        runOnMainSync(() -> {
+            absSwipeUpHandler.startNewTask(unused -> {});
+            verifyRecentsAnimationFinishedAndCallCallback();
+        });
+    }
+
+    @Test
+    public void testHomeGesture_finishesRecentsAnimationController() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+        runOnMainSync(() -> {
+            verify(mRecentsAnimationController).detachNavigationBarFromApp(true);
+            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;
+
+        doReturn(mState).when(mActivityInterface).stateFromGestureEndTarget(any());
+
+        SWIPE_HANDLER swipeHandler = createSwipeHandler(SystemClock.uptimeMillis(), isQuickSwitch);
+
+        swipeHandler.onActivityInit(/* alreadyOnHome= */ false);
+        swipeHandler.onGestureStarted(isQuickSwitch);
+        onRecentsAnimationStart(swipeHandler);
+
+        when(mGestureState.getRunningTaskIds(anyBoolean())).thenReturn(new int[0]);
+        runOnMainSync(swipeHandler::switchToScreenshot);
+
+        when(mGestureState.getEndTarget()).thenReturn(endTarget);
+        when(mGestureState.isRecentsAnimationRunning()).thenReturn(isQuickSwitch);
+        float xVelocityPxPerMs = isQuickSwitch ? 100 : 0;
+        float yVelocityPxPerMs = isQuickSwitch ? 0 : -100;
+        swipeHandler.onGestureEnded(
+                yVelocityPxPerMs, new PointF(xVelocityPxPerMs, yVelocityPxPerMs));
+        swipeHandler.onCalculateEndTarget();
+        runOnMainSync(swipeHandler::onSettledOnEndTarget);
+
+        return swipeHandler;
+    }
+
+    private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) {
+        when(mActivityInterface.getOverviewWindowBounds(any(), any())).thenReturn(new Rect());
+
+        runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart(
+                mRecentsAnimationController, mRecentsAnimationTargets));
+    }
+
+    protected static void runOnMainSync(Runnable runnable) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+
+    @NonNull
+    private SWIPE_HANDLER createSwipeHandler() {
+        return createSwipeHandler(SystemClock.uptimeMillis(), false);
+    }
+
+    @Nullable
+    protected RecentsWindowManager getRecentsWindowManager() {
+        return null;
+    }
+
+    @NonNull
+    protected abstract SWIPE_HANDLER createSwipeHandler(
+            long touchTimeMs, boolean continuingLastGesture);
+
+    @NonNull
+    protected abstract RecentsViewContainer getRecentsContainer();
+
+    @NonNull
+    protected abstract RECENTS_VIEW getRecentsView();
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
new file mode 100644
index 0000000..88197e5
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
@@ -0,0 +1,66 @@
+/*
+ * 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.test.filters.SmallTest;
+
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
+
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class FallbackSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase<
+        RecentsState,
+        RecentsActivity,
+        FallbackRecentsView<RecentsActivity>,
+        FallbackSwipeHandler,
+        FallbackActivityInterface> {
+
+    @Mock private RecentsActivity mRecentsActivity;
+    @Mock private FallbackRecentsView<RecentsActivity> mRecentsView;
+
+
+    @Override
+    protected FallbackSwipeHandler createSwipeHandler(
+            long touchTimeMs, boolean continuingLastGesture) {
+        return new FallbackSwipeHandler(
+                mContext,
+                mRecentsAnimationDeviceState,
+                mTaskAnimationManager,
+                mGestureState,
+                touchTimeMs,
+                continuingLastGesture,
+                mInputConsumerController);
+    }
+
+    @NonNull
+    @Override
+    protected RecentsActivity getRecentsContainer() {
+        return mRecentsActivity;
+    }
+
+    @NonNull
+    @Override
+    protected FallbackRecentsView<RecentsActivity> getRecentsView() {
+        return mRecentsView;
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt
new file mode 100644
index 0000000..24f9696
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt
@@ -0,0 +1,139 @@
+package com.android.quickstep
+
+/*
+ * 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.
+ */
+
+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
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED)
+class LauncherRestoreEventLoggerImplTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val mLauncherModelHelper = LauncherModelHelper()
+    private val mSandboxContext: SandboxModelContext = mLauncherModelHelper.sandboxContext
+    private lateinit var loggerUnderTest: LauncherRestoreEventLoggerImpl
+
+    @Before
+    fun setup() {
+        loggerUnderTest = LauncherRestoreEventLoggerImpl(mSandboxContext)
+    }
+
+    @After
+    fun teardown() {
+        loggerUnderTest.restoreEventLogger.clearData()
+        mLauncherModelHelper.destroy()
+    }
+
+    @Test
+    fun `logLauncherItemsRestoreFailed logs multiple items as failing restore`() {
+        // Given
+        val expectedDataType = "application"
+        val expectedError = "test_failure"
+        // When
+        loggerUnderTest.logLauncherItemsRestoreFailed(
+            dataType = expectedDataType,
+            count = 5,
+            error = expectedError
+        )
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(0)
+        assertThat(actualResult.failCount).isEqualTo(5)
+        assertThat(actualResult.errors.keys).containsExactly(expectedError)
+    }
+
+    @Test
+    fun `logLauncherItemsRestored logs multiple items as restored`() {
+        // Given
+        val expectedDataType = "application"
+        // When
+        loggerUnderTest.logLauncherItemsRestored(dataType = expectedDataType, count = 5)
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(5)
+        assertThat(actualResult.failCount).isEqualTo(0)
+        assertThat(actualResult.errors.keys).isEmpty()
+    }
+
+    @Test
+    fun `logSingleFavoritesItemRestored logs a single Favorites Item as restored`() {
+        // Given
+        val expectedDataType = "widget"
+        // When
+        loggerUnderTest.logSingleFavoritesItemRestored(favoritesId = Favorites.ITEM_TYPE_APPWIDGET)
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(1)
+        assertThat(actualResult.failCount).isEqualTo(0)
+        assertThat(actualResult.errors.keys).isEmpty()
+    }
+
+    @Test
+    fun `logSingleFavoritesItemRestoreFailed logs a single Favorites Item as failing restore`() {
+        // Given
+        val expectedDataType = "widget"
+        val expectedError = "test_failure"
+        // When
+        loggerUnderTest.logSingleFavoritesItemRestoreFailed(
+            favoritesId = Favorites.ITEM_TYPE_APPWIDGET,
+            error = expectedError
+        )
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(0)
+        assertThat(actualResult.failCount).isEqualTo(1)
+        assertThat(actualResult.errors.keys).containsExactly(expectedError)
+    }
+
+    @Test
+    fun `logFavoritesItemsRestoreFailed logs multiple Favorites Items as failing restore`() {
+        // Given
+        val expectedDataType = "deep_shortcut"
+        val expectedError = "test_failure"
+        // When
+        loggerUnderTest.logFavoritesItemsRestoreFailed(
+            favoritesId = Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+            count = 5,
+            error = expectedError
+        )
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(0)
+        assertThat(actualResult.failCount).isEqualTo(5)
+        assertThat(actualResult.errors.keys).containsExactly(expectedError)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
new file mode 100644
index 0000000..32b5b85
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 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.graphics.PointF
+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
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LauncherSwipeHandlerV2Test {
+
+    @Mock private lateinit var taskAnimationManager: TaskAnimationManager
+
+    private lateinit var gestureState: GestureState
+    @Mock private lateinit var inputConsumerController: InputConsumerController
+
+    @Mock private lateinit var systemUiProxy: SystemUiProxy
+
+    private lateinit var underTest: LauncherSwipeHandlerV2
+
+    @get:Rule val mockitoRule = MockitoJUnit.rule()
+
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = launcherModelHelper.sandboxContext
+
+    private val flingSpeed =
+        -(sandboxContext.resources.getDimension(R.dimen.quickstep_fling_threshold_speed) + 1)
+
+    @Before
+    fun setup() {
+        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))
+
+        underTest =
+            LauncherSwipeHandlerV2(
+                sandboxContext,
+                deviceState,
+                taskAnimationManager,
+                gestureState,
+                0,
+                false,
+                inputConsumerController,
+            )
+        underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false)
+    }
+
+    @Test
+    fun goHomeFromAppByTrackpad_updateEduStats() {
+        gestureState.setTrackpadGestureType(GestureState.TrackpadGestureType.THREE_FINGER)
+        underTest.onGestureEnded(flingSpeed, PointF())
+        verify(systemUiProxy)
+            .updateContextualEduStats(
+                /* isTrackpadGesture= */ eq(true),
+                eq(GestureType.HOME.toString()),
+            )
+    }
+
+    @Test
+    fun goHomeFromAppByTouch_updateEduStats() {
+        underTest.onGestureEnded(flingSpeed, PointF())
+        verify(systemUiProxy)
+            .updateContextualEduStats(
+                /* isTrackpadGesture= */ eq(false),
+                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
new file mode 100644
index 0000000..ec1dc8b
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
@@ -0,0 +1,95 @@
+/*
+ * 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 org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.WorkspaceStateTransitionAnimation;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+import com.android.quickstep.views.RecentsView;
+
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class LauncherSwipeHandlerV2TestCase extends AbsSwipeUpHandlerTestCase<
+        LauncherState,
+        QuickstepLauncher,
+        RecentsView<QuickstepLauncher, LauncherState>,
+        LauncherSwipeHandlerV2,
+        LauncherActivityInterface> {
+
+    @Mock private QuickstepLauncher mQuickstepLauncher;
+    @Mock private RecentsView<QuickstepLauncher, LauncherState> mRecentsView;
+    @Mock private Workspace<?> mWorkspace;
+    @Mock private Hotseat mHotseat;
+    @Mock private WorkspaceStateTransitionAnimation mTransitionAnimation;
+
+    @Before
+    public void setUpQuickStepLauncher() {
+        when(mQuickstepLauncher.createAtomicAnimationFactory())
+                .thenReturn(new AtomicAnimationFactory<>(0));
+        when(mQuickstepLauncher.getHotseat()).thenReturn(mHotseat);
+        doReturn(mWorkspace).when(mQuickstepLauncher).getWorkspace();
+        doReturn(new StateManager(mQuickstepLauncher, LauncherState.NORMAL))
+                .when(mQuickstepLauncher).getStateManager();
+
+    }
+
+    @Before
+    public void setUpWorkspace() {
+        when(mWorkspace.getStateTransitionAnimation()).thenReturn(mTransitionAnimation);
+    }
+
+    @NonNull
+    @Override
+    protected LauncherSwipeHandlerV2 createSwipeHandler(
+            long touchTimeMs, boolean continuingLastGesture) {
+        return new LauncherSwipeHandlerV2(
+                mContext,
+                mRecentsAnimationDeviceState,
+                mTaskAnimationManager,
+                mGestureState,
+                touchTimeMs,
+                continuingLastGesture,
+                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/MultiStateCallbackTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java
new file mode 100644
index 0000000..0ff142a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class MultiStateCallbackTest {
+
+    private int mFlagCount = 0;
+    private int getNextStateFlag() {
+        int index = 1 << mFlagCount;
+        mFlagCount++;
+        return index;
+    }
+
+    private final MultiStateCallback mMultiStateCallback = new MultiStateCallback(new String[0]);
+    private final Runnable mCallback = spy(new Runnable() {
+        @Override
+        public void run() {}
+    });
+    private final Consumer<Boolean> mListener = spy(new Consumer<Boolean>() {
+        @Override
+        public void accept(Boolean isOn) {}
+    });
+
+    @Test
+    public void testSetState_trackedProperly() {
+        int watchedAnime = getNextStateFlag();
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(0);
+        assertThat(mMultiStateCallback.hasStates(watchedAnime)).isFalse();
+
+        mMultiStateCallback.setState(watchedAnime);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime);
+        assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue();
+    }
+
+    @Test
+    public void testSetState_withMultipleStates_trackedProperly() {
+        int watchedAnime = getNextStateFlag();
+        int sharedMemes = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedAnime);
+        mMultiStateCallback.setState(sharedMemes);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime | sharedMemes);
+        assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue();
+        assertThat(mMultiStateCallback.hasStates(sharedMemes)).isTrue();
+        assertThat(mMultiStateCallback.hasStates(watchedAnime | sharedMemes)).isTrue();
+    }
+
+    @Test
+    public void testClearState_trackedProperly() {
+        int lovedAnime = getNextStateFlag();
+
+        mMultiStateCallback.setState(lovedAnime);
+        mMultiStateCallback.clearState(lovedAnime);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(0);
+        assertThat(mMultiStateCallback.hasStates(lovedAnime)).isFalse();
+    }
+
+    @Test
+    public void testClearState_withMultipleState_trackedProperly() {
+        int lovedAnime = getNextStateFlag();
+        int talkedAboutAnime = getNextStateFlag();
+
+        mMultiStateCallback.setState(lovedAnime);
+        mMultiStateCallback.setState(talkedAboutAnime);
+        mMultiStateCallback.clearState(talkedAboutAnime);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(lovedAnime);
+        assertThat(mMultiStateCallback.hasStates(lovedAnime)).isTrue();
+        assertThat(mMultiStateCallback.hasStates(talkedAboutAnime)).isFalse();
+        assertThat(mMultiStateCallback.hasStates(lovedAnime | talkedAboutAnime)).isFalse();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_withoutState() {
+        int watchedOnePiece = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedOnePiece, mCallback);
+
+        verify(mCallback, never()).run();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_whenNotTracked() {
+        int watchedJujutsuKaisen = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedJujutsuKaisen);
+
+        verify(mCallback, never()).run();
+    }
+
+    @Test
+    public void testCallbackRuns_afterTrackedAndStateSet() {
+        int watchedHunterXHunter = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback);
+        mMultiStateCallback.setState(watchedHunterXHunter);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_onUiThread() {
+        int watchedHunterXHunter = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback);
+        mMultiStateCallback.setStateOnUiThread(watchedHunterXHunter);
+
+        runOnMainSync(() -> verify(mCallback, times(1)).run());
+    }
+
+    @Test
+    public void testCallbackRuns_agnosticallyToCallOrder() {
+        int watchedFullMetalAlchemist = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedFullMetalAlchemist);
+        mMultiStateCallback.runOnceAtState(watchedFullMetalAlchemist, mCallback);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_onlyOnceAfterStateSet() {
+        int watchedBleach = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedBleach, mCallback);
+        mMultiStateCallback.setState(watchedBleach);
+        mMultiStateCallback.setState(watchedBleach);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_onlyOnceAfterClearState() {
+        int rememberedGreatShow = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(rememberedGreatShow, mCallback);
+        mMultiStateCallback.setState(rememberedGreatShow);
+        mMultiStateCallback.clearState(rememberedGreatShow);
+        mMultiStateCallback.setState(rememberedGreatShow);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_withoutFullStateSet() {
+        int watchedMobPsycho = getNextStateFlag();
+        int watchedVinlandSaga = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedMobPsycho | watchedVinlandSaga, mCallback);
+        mMultiStateCallback.setState(watchedMobPsycho);
+
+        verify(mCallback, times(0)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_withFullStateSet_agnosticallyToCallOrder() {
+        int watchedReZero = getNextStateFlag();
+        int watchedJojosBizareAdventure = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedJojosBizareAdventure);
+        mMultiStateCallback.runOnceAtState(watchedReZero | watchedJojosBizareAdventure, mCallback);
+        mMultiStateCallback.setState(watchedReZero);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_withFullStateSet_asIntegerMask() {
+        int watchedPokemon = getNextStateFlag();
+        int watchedDigimon = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedPokemon | watchedDigimon, mCallback);
+        mMultiStateCallback.setState(watchedPokemon | watchedDigimon);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_afterClearState() {
+        int watchedMonster = getNextStateFlag();
+        int watchedPingPong = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedMonster | watchedPingPong, mCallback);
+        mMultiStateCallback.setState(watchedMonster);
+        mMultiStateCallback.clearState(watchedMonster);
+        mMultiStateCallback.setState(watchedPingPong);
+
+        verify(mCallback, times(0)).run();
+    }
+
+    @Test
+    public void testlistenerRuns_multipleTimes() {
+        int watchedSteinsGate = getNextStateFlag();
+
+        mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener);
+        mMultiStateCallback.setState(watchedSteinsGate);
+
+        // Called exactly one
+        verify(mListener, times(1)).accept(anyBoolean());
+        // Called exactly once with isOn = true
+        verify(mListener, times(1)).accept(eq(true));
+        // Never called with isOn = false
+        verify(mListener, times(0)).accept(eq(false));
+
+        mMultiStateCallback.clearState(watchedSteinsGate);
+
+        // Called exactly twice
+        verify(mListener, times(2)).accept(anyBoolean());
+        // Called exactly once with isOn = true
+        verify(mListener, times(1)).accept(eq(true));
+        // Called exactly once with isOn = false
+        verify(mListener, times(1)).accept(eq(false));
+    }
+
+    @Test
+    public void testlistenerDoesNotRun_forUnchangedState() {
+        int watchedSteinsGate = getNextStateFlag();
+
+        mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener);
+        mMultiStateCallback.setState(watchedSteinsGate);
+        mMultiStateCallback.setState(watchedSteinsGate);
+
+        // State remained unchanged
+        verify(mListener, times(1)).accept(anyBoolean());
+        // Called exactly once with isOn = true
+        verify(mListener, times(1)).accept(eq(true));
+    }
+
+    private static void runOnMainSync(Runnable runnable) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
new file mode 100644
index 0000000..0ae710f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.platform.test.flag.junit.SetFlagsRule
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.TestDispatcherProvider
+import com.android.launcher3.util.rule.setFlags
+import com.android.quickstep.OverviewCommandHelper.CommandInfo
+import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus
+import com.android.quickstep.OverviewCommandHelper.CommandType
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class OverviewCommandHelperTest {
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+    private lateinit var sut: OverviewCommandHelper
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private var pendingCallbacksWithDelays = mutableListOf<Long>()
+
+    @Suppress("UNCHECKED_CAST")
+    @Before
+    fun setup() {
+        setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT)
+
+        sut =
+            spy(
+                OverviewCommandHelper(
+                    touchInteractionService = mock(),
+                    overviewComponentObserver = mock(),
+                    taskAnimationManager = mock(),
+                    dispatcherProvider = TestDispatcherProvider(dispatcher)
+                )
+            )
+
+        doAnswer { invocation ->
+                val pendingCallback = invocation.arguments[1] as () -> Unit
+
+                val delayInMillis = pendingCallbacksWithDelays.removeFirstOrNull()
+                if (delayInMillis != null) {
+                    runBlocking {
+                        testScope.backgroundScope.launch {
+                            delay(delayInMillis)
+                            pendingCallback.invoke()
+                        }
+                    }
+                }
+                delayInMillis == null // if no callback to execute, returns success
+            }
+            .`when`(sut)
+            .executeCommand(any<CommandInfo>(), any())
+    }
+
+    private fun addCallbackDelay(delayInMillis: Long = 0) {
+        pendingCallbacksWithDelays.add(delayInMillis)
+    }
+
+    @Test
+    fun whenFirstCommandIsAdded_executeCommandImmediately() =
+        testScope.runTest {
+            // Add command to queue
+            val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!!
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
+            runCurrent()
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    @Test
+    fun whenFirstCommandIsAdded_executeCommandImmediately_WithCallbackDelay() =
+        testScope.runTest {
+            addCallbackDelay(100)
+
+            // Add command to queue
+            val commandType = CommandType.HOME
+            val commandInfo: CommandInfo = sut.addCommand(commandType)!!
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
+
+            runCurrent()
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING)
+
+            advanceTimeBy(200L)
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    @Test
+    fun whenFirstCommandIsPendingCallback_NextCommandWillWait() =
+        testScope.runTest {
+            // Add command to queue
+            addCallbackDelay(100)
+            val commandType1 = CommandType.HOME
+            val commandInfo1: CommandInfo = sut.addCommand(commandType1)!!
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE)
+
+            addCallbackDelay(100)
+            val commandType2 = CommandType.SHOW
+            val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            runCurrent()
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            advanceTimeBy(101L)
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
+
+            advanceTimeBy(101L)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    @Test
+    fun whenCommandTakesTooLong_TriggerTimeout_AndExecuteNextCommand() =
+        testScope.runTest {
+            // Add command to queue
+            addCallbackDelay(QUEUE_TIMEOUT)
+            val commandType1 = CommandType.HOME
+            val commandInfo1: CommandInfo = sut.addCommand(commandType1)!!
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE)
+
+            addCallbackDelay(100)
+            val commandType2 = CommandType.SHOW
+            val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            runCurrent()
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            advanceTimeBy(QUEUE_TIMEOUT)
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
+
+            advanceTimeBy(101)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    private companion object {
+        const val QUEUE_TIMEOUT = 5001L
+    }
+}
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
new file mode 100644
index 0000000..98a3607
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -0,0 +1,453 @@
+/*
+ * 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 android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_HOVER_ENTER;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.SystemClock;
+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.util.DisplayController;
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.NavHandle;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.TopTaskTracker;
+import com.android.quickstep.util.TestExtensions;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavHandleLongPressInputConsumerTest {
+
+    private static final float TOUCH_SLOP = 10;
+    private static final float SQUARED_TOUCH_SLOP = 100;
+
+    private final AtomicBoolean mLongPressTriggered = new AtomicBoolean();
+    private final Runnable mLongPressRunnable = () -> mLongPressTriggered.set(true);
+    private NavHandleLongPressInputConsumer mUnderTest;
+    private SandboxContext mContext;
+    private float mScreenWidth;
+    @Mock InputConsumer mDelegate;
+    @Mock InputMonitorCompat mInputMonitor;
+    @Mock RecentsAnimationDeviceState mDeviceState;
+    @Mock NavHandle mNavHandle;
+    @Mock GestureState mGestureState;
+    @Mock NavHandleLongPressHandler mNavHandleLongPressHandler;
+    @Mock TopTaskTracker mTopTaskTracker;
+    @Mock TopTaskTracker.CachedTaskInfo mTaskInfo;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mTopTaskTracker.getCachedTopTask(anyBoolean())).thenReturn(mTaskInfo);
+        when(mDeviceState.getSquaredTouchSlop()).thenReturn(SQUARED_TOUCH_SLOP);
+        when(mDelegate.allowInterceptByParent()).thenReturn(true);
+        mLongPressTriggered.set(false);
+        when(mNavHandleLongPressHandler.getLongPressRunnable(any())).thenReturn(mLongPressRunnable);
+        initializeObjectUnderTest();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        MAIN_EXECUTOR.getHandler().removeCallbacks(mLongPressRunnable);
+        MAIN_EXECUTOR.submit(() -> null).get();
+        mContext.onDestroy();
+    }
+
+    @Test
+    public void testGetType() {
+        assertThat(mUnderTest.getType() & InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS).isNotEqualTo(0);
+    }
+
+    @Test
+    public void testDelegateDisallowsTouchIntercept() {
+        when(mDelegate.allowInterceptByParent()).thenReturn(false);
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+
+        verify(mDelegate).onMotionEvent(any());
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testDelegateDisallowsTouchInterceptAfterTouchDown() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+
+        // Delegate should still get touches unless long press is triggered.
+        verify(mDelegate).onMotionEvent(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+
+        when(mDelegate.allowInterceptByParent()).thenReturn(false);
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_MOVE));
+
+        // Delegate should still get motion events unless long press is triggered.
+        verify(mDelegate, times(2)).onMotionEvent(any());
+        // But our handler should be cancelled.
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggered() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+        assertTrue(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggeredWithSlightVerticalMovement() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                -(TOUCH_SLOP - 1)));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+        assertTrue(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggeredWithSlightHorizontalMovement() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+        assertTrue(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggeredWithExtendedTwoStageDuration() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                    mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
+            // We have entered the second stage, so the normal timeout shouldn't trigger.
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+
+            // After an extended time, the long press should trigger.
+            float extendedDurationMultiplier =
+                    (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
+                    * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+            assertTrue(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testLongPressTriggeredWithNormalDurationInFirstStage() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            // We have not entered the second stage, so the normal timeout should trigger.
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+            assertTrue(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchUp() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchCancel() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedVertically() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                -(TOUCH_SLOP + 1)));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedHorizontally() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                mScreenWidth / 2f - (TOUCH_SLOP + 1), 0));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedVertically_twoStageEnabled() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            // Enter the second stage.
+            mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                    -(TOUCH_SLOP - 1)));
+            // Normal duration shouldn't trigger.
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+
+            // Move out of the second stage.
+            mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                    -(TOUCH_SLOP + 1)));
+            // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
+            float extendedDurationMultiplier =
+                    (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
+                    * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            // Touch cancelled.
+            verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedHorizontally_twoStageEnabled() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            // Enter the second stage.
+            mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                    mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
+            // Normal duration shouldn't trigger.
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+
+            // Move out of the second stage.
+            mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                    mScreenWidth / 2f - (TOUCH_SLOP + 1), 0));
+            // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
+            float extendedDurationMultiplier =
+                    (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
+                    * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            // Touch cancelled.
+            verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testTouchOutsideNavHandleIgnored() {
+        // Touch the far left side of the screen. (y=0 is top of navbar region, picked arbitrarily)
+        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_DOWN, 0, 0));
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        // Should be ignored because the x position was not centered in the navbar region.
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testHoverPassedToDelegate() {
+        // Regardless of whether the delegate wants us to intercept, we tell it about hover events.
+        when(mDelegate.allowInterceptByParent()).thenReturn(false);
+        mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER));
+
+        verify(mDelegate).onHoverEvent(any());
+
+        when(mDelegate.allowInterceptByParent()).thenReturn(true);
+        mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER));
+
+        verify(mDelegate, times(2)).onHoverEvent(any());
+    }
+
+    private void initializeObjectUnderTest() {
+        if (mContext != null) {
+            mContext.onDestroy();
+        }
+        mContext = new SandboxContext(getApplicationContext());
+        mContext.putObject(TopTaskTracker.INSTANCE, mTopTaskTracker);
+        mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x;
+        mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
+                mDeviceState, mNavHandle, mGestureState);
+        mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler);
+    }
+
+    /** Generate a motion event centered horizontally in the screen. */
+    private MotionEvent generateCenteredMotionEvent(int motionAction) {
+        return generateCenteredMotionEventWithYOffset(motionAction, 0);
+    }
+
+    /** Generate a motion event centered horizontally in the screen, with y offset. */
+    private MotionEvent generateCenteredMotionEventWithYOffset(int motionAction, float y) {
+        return generateMotionEvent(motionAction, mScreenWidth / 2f, y);
+    }
+
+    private static MotionEvent generateMotionEvent(int motionAction, float x, float y) {
+        return MotionEvent.obtain(0, 0, motionAction, x, y, 0);
+    }
+
+    private static AutoCloseable overrideTwoStageFlag(boolean value) {
+        return TestExtensions.overrideNavConfigFlag(
+                "ENABLE_LPNH_TWO_STAGES",
+                value,
+                () -> DeviceConfigWrapper.get().getEnableLpnhTwoStages());
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
new file mode 100644
index 0000000..7c48ea4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.logging
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.ALLOW_ROTATION
+import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
+import com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY
+import com.android.launcher3.logging.InstanceId
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_DISABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON
+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.google.common.truth.Truth.assertThat
+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.atLeastOnce
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class SettingsChangeLoggerTest {
+    private val mContext: Context = ApplicationProvider.getApplicationContext()
+
+    private val mInstanceId = InstanceId.fakeInstanceId(1)
+
+    private lateinit var mSystemUnderTest: SettingsChangeLogger
+
+    @Mock private lateinit var mStatsLogManager: StatsLogManager
+
+    @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger
+
+    @Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
+
+    private var mDefaultThemedIcons = false
+    private var mDefaultAllowRotation = false
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(mStatsLogManager.logger()).doReturn(mMockLogger)
+        whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger)
+        mDefaultThemedIcons = LauncherPrefs.get(mContext).get(THEMED_ICONS)
+        mDefaultAllowRotation = LauncherPrefs.get(mContext).get(ALLOW_ROTATION)
+        // To match the default value of THEMED_ICONS
+        LauncherPrefs.get(mContext).put(THEMED_ICONS, false)
+        // To match the default value of ALLOW_ROTATION
+        LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false)
+
+        mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+    }
+
+    @After
+    fun tearDown() {
+        LauncherPrefs.get(mContext).put(THEMED_ICONS, mDefaultThemedIcons)
+        LauncherPrefs.get(mContext).put(ALLOW_ROTATION, mDefaultAllowRotation)
+    }
+
+    @Test
+    fun loggingPrefs_correctDefaultValue() {
+        val systemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+
+        assertThat(systemUnderTest.loggingPrefs[ALLOW_ROTATION_PREFERENCE_KEY]!!.defaultValue)
+            .isFalse()
+        assertThat(systemUnderTest.loggingPrefs[ADD_ICON_PREFERENCE_KEY]!!.defaultValue).isTrue()
+        assertThat(systemUnderTest.loggingPrefs[OVERVIEW_SUGGESTED_ACTIONS]!!.defaultValue).isTrue()
+        assertThat(systemUnderTest.loggingPrefs[SMARTSPACE_ON_HOME_SCREEN]!!.defaultValue).isTrue()
+        assertThat(systemUnderTest.loggingPrefs[KEY_ENABLE_MINUS_ONE]!!.defaultValue).isTrue()
+    }
+
+    @Test
+    fun logSnapshot_defaultValue() {
+        mSystemUnderTest.logSnapshot(mInstanceId)
+
+        verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+        val capturedEvents = mEventCaptor.allValues
+        assertThat(capturedEvents.isNotEmpty()).isTrue()
+        verifyDefaultEvent(capturedEvents)
+        assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_DISABLED.id })
+            .isTrue()
+    }
+
+    @Test
+    fun logSnapshot_updateAllowRotation() {
+        LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = true)
+
+        // This a new object so the values of mLoggablePrefs will be different
+        SettingsChangeLogger(mContext, mStatsLogManager).logSnapshot(mInstanceId)
+
+        verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+        val capturedEvents = mEventCaptor.allValues
+        assertThat(capturedEvents.isNotEmpty()).isTrue()
+        verifyDefaultEvent(capturedEvents)
+        assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_ENABLED.id })
+            .isTrue()
+    }
+
+    private fun verifyDefaultEvent(capturedEvents: MutableList<StatsLogManager.EventEnum>) {
+        assertThat(capturedEvents.any { it.id == LAUNCHER_NOTIFICATION_DOT_ENABLED.id }).isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON.id })
+            .isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_THEMED_ICON_DISABLED.id }).isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED.id })
+            .isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED.id })
+            .isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED.id })
+            .isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED }).isTrue()
+    }
+
+    companion object {
+        private const val KEY_ENABLE_MINUS_ONE = "pref_enable_minus_one"
+        private const val OVERVIEW_SUGGESTED_ACTIONS = "pref_overview_action_suggestions"
+        private const val SMARTSPACE_ON_HOME_SCREEN = "pref_smartspace_home_screen"
+
+        private const val LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED = 617
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
new file mode 100644
index 0000000..7d09efd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+
+class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier {
+    val listeners = mutableListOf<HighResLoadingStateChangedCallback>()
+
+    override fun addCallback(callback: HighResLoadingStateChangedCallback) {
+        listeners.add(callback)
+    }
+
+    override fun removeCallback(callback: HighResLoadingStateChangedCallback) {
+        listeners.remove(callback)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
new file mode 100644
index 0000000..fc2f029
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository {
+    private var recentsDeviceProfile =
+        RecentsDeviceProfile(
+            isLargeScreen = false,
+        )
+
+    override fun getRecentsDeviceProfile() = recentsDeviceProfile
+
+    fun setRecentsDeviceProfile(newValue: RecentsDeviceProfile) {
+        recentsDeviceProfile = newValue
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt
new file mode 100644
index 0000000..c328672
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.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.recents.data
+
+import android.view.Surface
+
+class FakeRecentsRotationStateRepository : RecentsRotationStateRepository {
+    private var recentsRotationState =
+        RecentsRotationState(
+            activityRotation = Surface.ROTATION_0,
+            orientationHandlerRotation = Surface.ROTATION_0
+        )
+
+    override fun getRecentsRotationState() = recentsRotationState
+
+    fun setRecentsRotationState(newValue: RecentsRotationState) {
+        recentsRotationState = newValue
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
index 242bc73..5de876a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -23,10 +23,13 @@
 import com.android.systemui.shared.recents.model.Task
 import com.google.common.truth.Truth.assertThat
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 class FakeTaskIconDataSource : TaskIconDataSource {
 
-    val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mock() }
+    val taskIdToDrawable: MutableMap<Int, Drawable> =
+        (0..10).associateWith { mockCopyableDrawable() }.toMutableMap()
+
     val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
     var shouldLoadSynchronously: Boolean = true
 
@@ -49,6 +52,19 @@
         }
         return null
     }
+
+    companion object {
+        fun mockCopyableDrawable(): Drawable {
+            val mutableDrawable = mock<Drawable>()
+            val immutableDrawable =
+                mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
+            val constantState =
+                mock<Drawable.ConstantState>().apply {
+                    whenever(newDrawable()).thenReturn(immutableDrawable)
+                }
+            return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
+        }
+    }
 }
 
 fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index 30fc491..d12c0b0 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -27,7 +27,8 @@
 
 class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
 
-    val taskIdToBitmap: Map<Int, Bitmap> = (0..10).associateWith { mock() }
+    val taskIdToBitmap: MutableMap<Int, Bitmap> =
+        (0..10).associateWith { mock<Bitmap>() }.toMutableMap()
     val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
     var shouldLoadSynchronously: Boolean = true
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt
new file mode 100644
index 0000000..765f0d1
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.TaskVisualsChangeListener
+
+class FakeTaskVisualsChangeNotifier : TaskVisualsChangeNotifier {
+    val listeners = mutableListOf<TaskVisualsChangeListener>()
+
+    override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) {
+        listeners.remove(listener)
+    }
+}
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 19990a8..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,28 +16,52 @@
 
 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
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 
 class FakeTasksRepository : RecentTasksRepository {
     private var thumbnailDataMap: Map<Int, ThumbnailData> = 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
 
     override fun getTaskDataById(taskId: Int): Flow<Task?> =
-        getAllTaskData().map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+        combine(getAllTaskData(), visibleTasks) { taskList, visibleTasks ->
+                taskList.filter { visibleTasks.contains(it.key.id) }
+            }
+            .map { taskList ->
+                val task = taskList.firstOrNull { it.key.id == taskId } ?: return@map null
+                Task(task).apply {
+                    thumbnail = task.thumbnail
+                    icon = task.icon
+                    titleDescription = task.titleDescription
+                    title = task.title
+                }
+            }
 
     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] } }
+        tasks.value =
+            tasks.value.map {
+                it.apply {
+                    thumbnail = thumbnailDataMap[it.key.id]
+                    taskIconDataMap[it.key.id].let { data ->
+                        title = data?.title
+                        titleDescription = data?.titleDescription
+                        icon = data?.icon
+                    }
+                }
+            }
     }
 
     fun seedTasks(tasks: List<Task>) {
@@ -47,4 +71,15 @@
     fun seedThumbnailData(thumbnailDataMap: Map<Int, ThumbnailData>) {
         this.thumbnailDataMap = thumbnailDataMap
     }
+
+    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/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
new file mode 100644
index 0000000..abe4142
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.FakeInvariantDeviceProfileTest
+import com.android.quickstep.views.RecentsViewContainer
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [RecentsDeviceProfileRepositoryImpl] */
+@RunWith(AndroidJUnit4::class)
+class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() {
+    private val recentsViewContainer = mock<RecentsViewContainer>()
+
+    private val systemUnderTest = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
+
+    @Test
+    fun deviceProfileMappedCorrectly() {
+        initializeVarsForTablet()
+        val tabletDeviceProfile = newDP()
+        whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
+
+        assertThat(systemUnderTest.getRecentsDeviceProfile())
+            .isEqualTo(RecentsDeviceProfile(isLargeScreen = true))
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt
new file mode 100644
index 0000000..017f037
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.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.quickstep.recents.data
+
+import android.view.Surface.ROTATION_270
+import android.view.Surface.ROTATION_90
+import com.android.quickstep.orientation.SeascapePagedViewHandler
+import com.android.quickstep.util.RecentsOrientedState
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [RecentsRotationStateRepositoryImpl] */
+class RecentsRotationStateRepositoryImplTest {
+    private val recentsOrientedState = mock<RecentsOrientedState>()
+
+    private val systemUnderTest = RecentsRotationStateRepositoryImpl(recentsOrientedState)
+
+    @Test
+    fun orientedStateMappedCorrectly() {
+        whenever(recentsOrientedState.recentsActivityRotation).thenReturn(ROTATION_90)
+        whenever(recentsOrientedState.orientationHandler).thenReturn(SeascapePagedViewHandler())
+
+        assertThat(systemUnderTest.getRecentsRotationState())
+            .isEqualTo(
+                RecentsRotationState(
+                    activityRotation = ROTATION_90,
+                    orientationHandlerRotation = ROTATION_270
+                )
+            )
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
new file mode 100644
index 0000000..41f6bfd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.UserHandle
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+class TaskVisualsChangedDelegateTest {
+    private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
+    private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
+
+    val systemUnderTest =
+        TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)
+
+    @Test
+    fun addingFirstListener_addsListenerToNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+
+        assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest)
+        assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest)
+    }
+
+    @Test
+    fun addingAndRemovingListener_removesListenerFromNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+        systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1))
+
+        assertThat(taskVisualsChangeNotifier.listeners).isEmpty()
+        assertThat(highResLoadingStateNotifier.listeners).isEmpty()
+    }
+
+    @Test
+    fun addingTwoAndRemovingOneListener_doesNotRemoveListenerFromNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 2), mock())
+        systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1))
+
+        assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest)
+        assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest)
+    }
+
+    @Test
+    fun onTaskIconChangedWithTaskId_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskIconChangedCallback>()
+        val additionalListener = mock<TaskIconChangedCallback>()
+        systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 1), expectedListener)
+        systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 2), additionalListener)
+
+        systemUnderTest.onTaskIconChanged(1)
+
+        verify(expectedListener).onTaskIconChanged()
+        verifyNoMoreInteractions(additionalListener)
+    }
+
+    @Test
+    fun onTaskIconChangedWithoutTaskId_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskIconChangedCallback>()
+        val listener = mock<TaskIconChangedCallback>()
+        // Correct match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            expectedListener
+        )
+        // 1 out of 2 match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1),
+            listener
+        )
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2),
+            listener
+        )
+        // 0 out of 2 match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2),
+            listener
+        )
+
+        systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
+
+        verify(expectedListener).onTaskIconChanged()
+        verifyNoMoreInteractions(listener)
+    }
+
+    @Test
+    fun replacedTaskIconChangedCallbacks_notCalled() {
+        val replacedListener = mock<TaskIconChangedCallback>()
+        val newListener = mock<TaskIconChangedCallback>()
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            replacedListener
+        )
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            newListener
+        )
+
+        systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
+
+        verifyNoMoreInteractions(replacedListener)
+        verify(newListener).onTaskIconChanged()
+    }
+
+    @Test
+    fun onTaskThumbnailChanged_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskThumbnailChangedCallback>()
+        val additionalListener = mock<TaskThumbnailChangedCallback>()
+        val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            expectedListener
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 2),
+            additionalListener
+        )
+
+        systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+        verify(expectedListener).onTaskThumbnailChanged(expectedThumbnailData)
+        verifyNoMoreInteractions(additionalListener)
+    }
+
+    @Test
+    fun onHighResLoadingStateChanged_notifiesAllListeners() {
+        val expectedListener = mock<TaskThumbnailChangedCallback>()
+        val additionalListener = mock<TaskThumbnailChangedCallback>()
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            expectedListener
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 2),
+            additionalListener
+        )
+
+        systemUnderTest.onHighResLoadingStateChanged(true)
+
+        verify(expectedListener).onHighResLoadingStateChanged()
+        verify(additionalListener).onHighResLoadingStateChanged()
+    }
+
+    @Test
+    fun replacedTaskThumbnailChangedCallbacks_notCalled() {
+        val replacedListener1 = mock<TaskThumbnailChangedCallback>()
+        val newListener1 = mock<TaskThumbnailChangedCallback>()
+        val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            replacedListener1
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1)
+
+        systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+        verifyNoMoreInteractions(replacedListener1)
+        verify(newListener1).onTaskThumbnailChanged(expectedThumbnailData)
+    }
+
+    private fun createTaskKey(id: Int = 1, pkg: String = PACKAGE_NAME, userId: Int = 1) =
+        TaskKey(id, 0, Intent().setPackage(pkg), ComponentName("", ""), userId, 0)
+
+    private companion object {
+        const val PACKAGE_NAME = "com.test.test"
+        const val ALTERNATIVE_PACKAGE_NAME = "com.test.test2"
+    }
+}
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 88fa190..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
@@ -18,174 +18,304 @@
 
 import android.content.ComponentName
 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.util.DesktopTask
 import com.android.quickstep.util.GroupTask
 import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @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()
     private val taskIconDataSource = FakeTaskIconDataSource()
+    private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
+    private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
+    private val taskVisualsChangedDelegate =
+        TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)
 
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(dispatcher)
     private val systemUnderTest =
-        TasksRepository(recentsModel, taskThumbnailDataSource, taskIconDataSource)
+        TasksRepository(
+            recentsModel,
+            taskThumbnailDataSource,
+            taskIconDataSource,
+            taskVisualsChangedDelegate,
+            testScope.backgroundScope,
+            TestDispatcherProvider(dispatcher),
+        )
 
     @Test
-    fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-
-        assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks)
-    }
-
-    @Test
-    fun getTaskDataByIdReturnsSpecificTask() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2])
-    }
-
-    @Test
-    fun setVisibleTasksPopulatesThumbnails() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // .drop(1) to ignore initial null content before from thumbnail was loaded.
-        assertThat(systemUnderTest.getTaskDataById(1).drop(1).first()!!.thumbnail!!.thumbnail)
-            .isEqualTo(bitmap1)
-        assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
-            .isEqualTo(bitmap2)
-    }
-
-    @Test
-    fun setVisibleTasksPopulatesIcons() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // .drop(1) to ignore initial null content before from thumbnail was loaded.
-        systemUnderTest
-            .getTaskDataById(1)
-            .drop(1)
-            .first()!!
-            .assertHasIconDataFromSource(taskIconDataSource)
-        systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
-    }
-
-    @Test
-    fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // .drop(1) to ignore initial null content before from thumbnail was loaded.
-        assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail)
-            .isEqualTo(bitmap2)
-
-        // Prevent new loading of Bitmaps
-        taskThumbnailDataSource.shouldLoadSynchronously = false
-        systemUnderTest.setVisibleTasks(listOf(2, 3))
-
-        assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
-            .isEqualTo(bitmap2)
-    }
-
-    @Test
-    fun changingVisibleTasksContainsAlreadyPopulatedIcons() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // .drop(1) to ignore initial null content before from icon was loaded.
-        systemUnderTest
-            .getTaskDataById(2)
-            .drop(1)
-            .first()!!
-            .assertHasIconDataFromSource(taskIconDataSource)
-
-        // Prevent new loading of Drawables
-        taskThumbnailDataSource.shouldLoadSynchronously = false
-        systemUnderTest.setVisibleTasks(listOf(2, 3))
-
-        systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
-    }
-
-    @Test
-    fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // .drop(1) to ignore initial null content before from thumbnail was loaded.
-        val task2 = systemUnderTest.getTaskDataById(2).drop(1).first()!!
-        assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
-        task2.assertHasIconDataFromSource(taskIconDataSource)
-
-        // Prevent new loading of Bitmaps
-        taskThumbnailDataSource.shouldLoadSynchronously = false
-        taskIconDataSource.shouldLoadSynchronously = false
-        systemUnderTest.setVisibleTasks(listOf(0, 1))
-
-        val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
-        assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
-        assertThat(task2AfterVisibleTasksChanged.icon).isNull()
-        assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull()
-        assertThat(task2AfterVisibleTasksChanged.title).isNull()
-    }
-
-    @Test
-    fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = runTest {
-        // Setup fakes
-        recentsModel.seedTasks(defaultTaskList)
-        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-        taskThumbnailDataSource.shouldLoadSynchronously = false
-
-        // Setup TasksRepository
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // Assert there is no bitmap in first emission
-        val taskFlow = systemUnderTest.getTaskDataById(2)
-        val taskFlowValuesList = mutableListOf<Task?>()
-        backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
-            taskFlow.toList(taskFlowValuesList)
+    fun getAllTaskDataReturnsFlattenedListOfTasks() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks)
         }
-        assertThat(taskFlowValuesList[0]!!.thumbnail).isNull()
 
-        // Simulate bitmap loading after first emission
-        taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke()
+    @Test
+    fun getTaskDataByIdReturnsSpecificTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
 
-        // Check for second emission
-        assertThat(taskFlowValuesList[1]!!.thumbnail!!.thumbnail).isEqualTo(bitmap2)
-    }
+            assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2])
+        }
+
+    @Test
+    fun setVisibleTasksPopulatesThumbnails() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
+            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+
+            assertThat(systemUnderTest.getTaskDataById(1).first()!!.thumbnail!!.thumbnail)
+                .isEqualTo(bitmap1)
+            assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
+                .isEqualTo(bitmap2)
+        }
+
+    @Test
+    fun setVisibleTasksPopulatesIcons() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+
+            systemUnderTest
+                .getTaskDataById(1)
+                .first()!!
+                .assertHasIconDataFromSource(taskIconDataSource)
+            systemUnderTest
+                .getTaskDataById(2)
+                .first()!!
+                .assertHasIconDataFromSource(taskIconDataSource)
+        }
+
+    @Test
+    fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+
+            assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
+                .isEqualTo(bitmap2)
+
+            // Prevent new loading of Bitmaps
+            taskThumbnailDataSource.shouldLoadSynchronously = false
+            systemUnderTest.setVisibleTasks(setOf(2, 3))
+
+            assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
+                .isEqualTo(bitmap2)
+        }
+
+    @Test
+    fun changingVisibleTasksContainsAlreadyPopulatedIcons() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+
+            systemUnderTest
+                .getTaskDataById(2)
+                .first()!!
+                .assertHasIconDataFromSource(taskIconDataSource)
+
+            // Prevent new loading of Drawables
+            taskThumbnailDataSource.shouldLoadSynchronously = false
+            systemUnderTest.setVisibleTasks(setOf(2, 3))
+
+            systemUnderTest
+                .getTaskDataById(2)
+                .first()!!
+                .assertHasIconDataFromSource(taskIconDataSource)
+        }
+
+    @Test
+    fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+
+            val task2 = systemUnderTest.getTaskDataById(2).first()!!
+            assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
+            task2.assertHasIconDataFromSource(taskIconDataSource)
+
+            // Prevent new loading of Bitmaps
+            taskThumbnailDataSource.shouldLoadSynchronously = false
+            taskIconDataSource.shouldLoadSynchronously = false
+            systemUnderTest.setVisibleTasks(setOf(0, 1))
+
+            val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
+            assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
+            assertThat(task2AfterVisibleTasksChanged.icon).isNull()
+            assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull()
+            assertThat(task2AfterVisibleTasksChanged.title).isNull()
+        }
+
+    @Test
+    fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() =
+        testScope.runTest {
+            // Setup fakes
+            recentsModel.seedTasks(defaultTaskList)
+            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+            taskThumbnailDataSource.shouldLoadSynchronously = false
+
+            // Setup TasksRepository
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+
+            // Assert there is no bitmap in first emission
+            assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
+
+            // Simulate bitmap loading after first emission
+            taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke()
+
+            // Check for second emission
+            assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
+                .isEqualTo(bitmap2)
+        }
+
+    @Test
+    fun onTaskThumbnailChanged_setsNewThumbnailDataOnTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+            systemUnderTest.setVisibleTasks(setOf(1))
+
+            val expectedThumbnailData = createThumbnailData()
+            val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
+            }
+            taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+            assertThat(task1ThumbnailValues.first()!!.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData)
+        }
+
+    @Test
+    fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1))
+
+            val expectedBitmap = mock<Bitmap>()
+            val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1ThumbnailValues = mutableListOf<Bitmap?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
+            }
+
+            taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+            taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
+
+            assertThat(task1ThumbnailValues.first()).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
+        }
+
+    @Test
+    fun onTaskIconChanged_setsNewIconOnTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1))
+
+            val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable()
+            val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1IconValues = mutableListOf<Drawable?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.icon }.toList(task1IconValues)
+            }
+            taskIconDataSource.taskIdToDrawable[1] = expectedIcon
+            taskVisualsChangedDelegate.onTaskIconChanged(1)
+
+            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(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
new file mode 100644
index 0000000..bd7d970
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.view.Surface.ROTATION_90
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Test for [GetThumbnailPositionUseCase] */
+@RunWith(AndroidJUnit4::class)
+class GetThumbnailPositionUseCaseTest {
+    private val task =
+        Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.BLACK
+        }
+    private val thumbnailData =
+        ThumbnailData(
+            thumbnail =
+                mock<Bitmap>().apply {
+                    whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                    whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                }
+        )
+
+    private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
+    private val rotationStateRepository = FakeRecentsRotationStateRepository()
+    private val tasksRepository = FakeTasksRepository()
+    private val previewPositionHelper = mock<PreviewPositionHelper>()
+
+    private val systemUnderTest =
+        GetThumbnailPositionUseCase(
+            deviceProfileRepository,
+            rotationStateRepository,
+            tasksRepository,
+            previewPositionHelper,
+        )
+
+    @Test
+    fun invisibleTask_returnsIdentityMatrix() = runTest {
+        tasksRepository.seedTasks(listOf(task))
+
+        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
+            .isInstanceOf(MissingThumbnail::class.java)
+    }
+
+    @Test
+    fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
+        tasksRepository.seedTasks(listOf(task))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
+
+        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
+            .isInstanceOf(MissingThumbnail::class.java)
+    }
+
+    @Test
+    fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
+        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+        tasksRepository.seedTasks(listOf(task))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
+
+        val isLargeScreen = true
+        deviceProfileRepository.setRecentsDeviceProfile(
+            deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen)
+        )
+        val activityRotation = ROTATION_90
+        rotationStateRepository.setRecentsRotationState(
+            rotationStateRepository
+                .getRecentsRotationState()
+                .copy(activityRotation = activityRotation)
+        )
+        val isRtl = true
+        val isRotated = true
+
+        whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
+        whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated)
+
+        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(MatrixScaling(MATRIX, isRotated))
+
+        verify(previewPositionHelper)
+            .updateThumbnailMatrix(
+                Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT),
+                thumbnailData,
+                CANVAS_WIDTH,
+                CANVAS_HEIGHT,
+                isLargeScreen,
+                activityRotation,
+                isRtl,
+            )
+    }
+
+    companion object {
+        const val TASK_ID = 2
+        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/util/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
similarity index 95%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 414f8ca..73aa460 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.quickstep.task.util
+package com.android.quickstep.recents.usecase
 
 import android.content.ComponentName
 import android.content.Intent
@@ -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
new file mode 100644
index 0000000..92f2efd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.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.quickstep.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [SysUiStatusNavFlagsUseCase] */
+class SysUiStatusNavFlagsUseCaseTest {
+    private lateinit var tasksRepository: FakeTasksRepository
+    private lateinit var sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase
+
+    @Before
+    fun setup() {
+        tasksRepository = FakeTasksRepository()
+        sysUiStatusNavFlagsUseCase = SysUiStatusNavFlagsUseCase(tasksRepository)
+        initTaskRepository()
+    }
+
+    @Test
+    fun onLightAppearanceReturnExpectedFlags() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(FIRST_TASK_ID))
+            .isEqualTo(FLAGS_APPEARANCE_LIGHT_THEME)
+    }
+
+    @Test
+    fun onDarkAppearanceReturnExpectedFlags() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(SECOND_TASK_ID))
+            .isEqualTo(FLAGS_APPEARANCE_DARK_THEME)
+    }
+
+    @Test
+    fun whenThumbnailIsNullReturnDefault() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(UNKNOWN_TASK_ID))
+            .isEqualTo(FLAGS_DEFAULT)
+    }
+
+    private fun initTaskRepository() {
+        val firstTask =
+            Task(Task.TaskKey(FIRST_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+                colorBackground = Color.BLACK
+            }
+        val firstThumbnailData =
+            ThumbnailData(
+                thumbnail =
+                    mock<Bitmap>().apply {
+                        whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                        whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                    },
+                appearance = APPEARANCE_LIGHT_THEME,
+            )
+
+        val secondTask =
+            Task(Task.TaskKey(SECOND_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2005)).apply {
+                colorBackground = Color.BLACK
+            }
+        val secondThumbnailData =
+            ThumbnailData(
+                thumbnail =
+                    mock<Bitmap>().apply {
+                        whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                        whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                    },
+                appearance = APPEARANCE_DARK_THEME,
+            )
+
+        tasksRepository.seedTasks(listOf(firstTask, secondTask))
+        tasksRepository.seedThumbnailData(
+            mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
+        )
+        tasksRepository.setVisibleTasks(setOf(FIRST_TASK_ID, SECOND_TASK_ID))
+    }
+
+    companion object {
+        const val FIRST_TASK_ID = 0
+        const val SECOND_TASK_ID = 100
+        const val UNKNOWN_TASK_ID = 404
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+        const val APPEARANCE_LIGHT_THEME = 24
+        const val FLAGS_APPEARANCE_LIGHT_THEME = 5
+        const val APPEARANCE_DARK_THEME = 0
+        const val FLAGS_APPEARANCE_DARK_THEME = 10
+        const val FLAGS_DEFAULT = 0
+    }
+}
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
new file mode 100644
index 0000000..a32e07d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.viewmodel
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.view.Surface
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class RecentsViewModelTest {
+    private val tasksRepository = FakeTasksRepository()
+    private val recentsViewData = RecentsViewData()
+    private val systemUnderTest = RecentsViewModel(tasksRepository, recentsViewData)
+
+    private val tasks = (0..5).map(::createTaskWithId)
+
+    @Test
+    fun taskVisibilityControlThumbnailsAvailability() = runTest {
+        val thumbnailData1 = createThumbnailData()
+        val thumbnailData2 = createThumbnailData()
+        tasksRepository.seedTasks(tasks)
+        tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2))
+
+        val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1)
+        val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2)
+
+        systemUnderTest.refreshAllTaskData()
+
+        assertThat(thumbnailDataFlow1.first()).isNull()
+        assertThat(thumbnailDataFlow2.first()).isNull()
+
+        systemUnderTest.updateVisibleTasks(listOf(1, 2))
+
+        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
+        assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2)
+
+        systemUnderTest.updateVisibleTasks(listOf(1))
+
+        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
+        assertThat(thumbnailDataFlow2.first()).isNull()
+
+        systemUnderTest.onReset()
+
+        assertThat(thumbnailDataFlow1.first()).isNull()
+        assertThat(thumbnailDataFlow2.first()).isNull()
+    }
+
+    @Test
+    fun updatesRunningTaskShowScreenshot() = runTest {
+        systemUnderTest.setRunningTaskShowScreenshot(true)
+        systemUnderTest.waitForRunningTaskShowScreenshotToUpdate()
+    }
+
+    @Test
+    fun waitForThumbnailsToUpdate() = runTest {
+        // Given taskRepository with visible 2 tasks containing thumbnailData
+        val thumbnailData1 = createThumbnailData().apply { snapshotId = 1 }
+        val thumbnailData2 = createThumbnailData().apply { snapshotId = 2 }
+        tasksRepository.seedTasks(tasks)
+        tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2))
+        systemUnderTest.updateVisibleTasks(listOf(1, 2))
+
+        val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1)
+        val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2)
+
+        // Then getThumbnailById should initially contains correct thumbnailData
+        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
+        assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2)
+
+        // When thumbnailData is updated in taskRepository
+        tasksRepository.seedThumbnailData(
+            mapOf(1 to thumbnailData1, 2 to createThumbnailData().apply { snapshotId = 3 })
+        )
+        // setVisibleTasks forces FakeTasksRepository to update the flows returned by
+        // getThumbnailById
+        tasksRepository.setVisibleTasks(setOf(1, 2))
+
+        // Then wait for thumbnailData should complete, and the previous getThumbnailById flow
+        // should return updated values
+        systemUnderTest.waitForThumbnailsToUpdate(
+            mapOf(2 to createThumbnailData().apply { snapshotId = 3 })
+        )
+        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
+        assertThat(thumbnailDataFlow2.first()?.snapshotId).isEqualTo(3)
+    }
+
+    @Test
+    fun waitForThumbnailsToUpdate_emptyMap() = runTest {
+        systemUnderTest.waitForThumbnailsToUpdate(emptyMap())
+    }
+
+    @Test
+    fun waitForThumbnailsToUpdate_null() = runTest {
+        systemUnderTest.waitForThumbnailsToUpdate(null)
+    }
+
+    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
+    }
+}
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
new file mode 100644
index 0000000..0767fb9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.view.Surface
+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.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+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.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class SplashAlphaUseCaseTest {
+    private val recentsViewData = RecentsViewData()
+    private val taskContainerData = TaskContainerData()
+    private val taskThumbnailViewData = TaskThumbnailViewData()
+    private val recentTasksRepository = FakeTasksRepository()
+    private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
+    private val systemUnderTest =
+        SplashAlphaUseCase(
+            recentsViewData,
+            taskContainerData,
+            taskThumbnailViewData,
+            recentTasksRepository,
+            recentsRotationStateRepository,
+        )
+
+    @Test
+    fun execute_withNullThumbnail_showsSplash() = runTest {
+        assertThat(systemUnderTest.execute(0).first()).isEqualTo(SPLASH_HIDDEN)
+    }
+
+    @Test
+    fun execute_withTaskSpecificSplashAlpha_showsSplash() = runTest {
+        setupTask(2)
+        taskContainerData.thumbnailSplashProgress.value = 0.7f
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.7f)
+    }
+
+    @Test
+    fun execute_withNoGlobalSplashEnabled_doesntShowSplash() = runTest {
+        setupTask(2)
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
+    }
+
+    @Test
+    fun execute_withSameAspectRatioAndRotation_withGlobalSplashEnabled_doesntShowSplash() =
+        runTest {
+            setupTask(2)
+            recentsViewData.thumbnailSplashProgress.value = 0.5f
+            taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
+            taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+            assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
+        }
+
+    @Test
+    fun execute_withDifferentAspectRatioAndSameRotation_showsSplash() = runTest {
+        setupTask(2)
+        recentsViewData.thumbnailSplashProgress.value = 0.5f
+        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
+        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+    }
+
+    @Test
+    fun execute_withSameAspectRatioAndDifferentRotation_showsSplash() = runTest {
+        setupTask(2, createThumbnailData(rotation = ROTATION_90))
+        recentsViewData.thumbnailSplashProgress.value = 0.5f
+        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
+        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+    }
+
+    @Test
+    fun execute_withDifferentAspectRatioAndRotation_showsSplash() = runTest {
+        setupTask(2, createThumbnailData(rotation = ROTATION_90))
+        recentsViewData.thumbnailSplashProgress.value = 0.5f
+        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
+        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+    }
+
+    private val tasks = (0..5).map(::createTaskWithId)
+
+    private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
+        recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
+        val expectedIconData = mock<Drawable>()
+        recentTasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+        recentTasksRepository.seedTasks(tasks)
+        recentTasksRepository.setVisibleTasks(setOf(taskId))
+    }
+
+    private fun createThumbnailData(
+        rotation: Int = Surface.ROTATION_0,
+        width: Int = THUMBNAIL_WIDTH,
+        height: Int = THUMBNAIL_HEIGHT,
+    ): ThumbnailData {
+        val bitmap = mock<Bitmap>()
+        whenever(bitmap.width).thenReturn(width)
+        whenever(bitmap.height).thenReturn(height)
+
+        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+    }
+
+    private fun createTaskWithId(taskId: Int) =
+        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+        }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+
+        const val SPLASH_HIDDEN = 0f
+        const val SPLASH_SHOWN = 1f
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
new file mode 100644
index 0000000..e3a6adf
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.drawable.Drawable
+import android.view.Surface
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
+import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [TaskThumbnailView] */
+@RunWith(AndroidJUnit4::class)
+class TaskThumbnailViewModelImplTest {
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private var taskViewType = TaskViewType.SINGLE
+    private val recentsViewData = RecentsViewData()
+    private val taskViewData by lazy { TaskViewData(taskViewType) }
+    private val taskContainerData = TaskContainerData()
+    private val dispatcherProvider = TestDispatcherProvider(dispatcher)
+    private val tasksRepository = FakeTasksRepository()
+    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+    private val splashAlphaUseCase: SplashAlphaUseCase = mock()
+
+    private val systemUnderTest by lazy {
+        TaskThumbnailViewModelImpl(
+            recentsViewData,
+            taskViewData,
+            taskContainerData,
+            dispatcherProvider,
+            tasksRepository,
+            mGetThumbnailPositionUseCase,
+            splashAlphaUseCase,
+        )
+    }
+
+    private val tasks = (0..5).map(::createTaskWithId)
+
+    @Test
+    fun initialStateIsUninitialized() =
+        testScope.runTest { assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) }
+
+    @Test
+    fun bindRunningTask_thenStateIs_LiveTile() =
+        testScope.runTest {
+            val taskId = 1
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+            recentsViewData.runningTaskIds.value = setOf(taskId)
+            systemUnderTest.bind(taskId)
+
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+        }
+
+    @Test
+    fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() =
+        testScope.runTest {
+            val taskId = 1
+            val expectedThumbnailData = createThumbnailData()
+            tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+            recentsViewData.runningTaskIds.value = setOf(taskId)
+            recentsViewData.runningTaskShowScreenshot.value = true
+            systemUnderTest.bind(taskId)
+
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(
+                    SnapshotSplash(
+                        Snapshot(
+                            backgroundColor = Color.rgb(1, 1, 1),
+                            bitmap = expectedThumbnailData.thumbnail!!,
+                            thumbnailRotation = Surface.ROTATION_0,
+                        ),
+                        expectedIconData,
+                    )
+                )
+        }
+
+    @Test
+    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() =
+        testScope.runTest {
+            recentsViewData.fullscreenProgress.value = 0.5f
+
+            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
+
+            recentsViewData.fullscreenProgress.value = 0.6f
+
+            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f)
+        }
+
+    @Test
+    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() =
+        testScope.runTest {
+            taskViewType = TaskViewType.DESKTOP
+            recentsViewData.fullscreenProgress.value = 0.5f
+
+            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
+
+            recentsViewData.fullscreenProgress.value = 0.6f
+
+            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
+        }
+
+    @Test
+    fun setAncestorScales_thenScaleIsCalculated() =
+        testScope.runTest {
+            recentsViewData.scale.value = 0.5f
+            taskViewData.scale.value = 0.6f
+
+            assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
+        }
+
+    @Test
+    fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
+        testScope.runTest {
+            val runningTaskId = 1
+            val stoppedTaskId = 2
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
+            recentsViewData.runningTaskIds.value = setOf(runningTaskId)
+            systemUnderTest.bind(runningTaskId)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+
+            systemUnderTest.bind(stoppedTaskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() =
+        testScope.runTest {
+            val stoppedTaskId = 2
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
+
+            systemUnderTest.bind(stoppedTaskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() =
+        testScope.runTest {
+            val taskId = 2
+            tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
+            tasks[taskId].isLocked = true
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+
+            systemUnderTest.bind(taskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() =
+        testScope.runTest {
+            val taskId = 2
+            val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
+            tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+
+            systemUnderTest.bind(taskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(
+                    SnapshotSplash(
+                        Snapshot(
+                            backgroundColor = Color.rgb(2, 2, 2),
+                            bitmap = expectedThumbnailData.thumbnail!!,
+                            thumbnailRotation = Surface.ROTATION_270,
+                        ),
+                        expectedIconData,
+                    )
+                )
+        }
+
+    @Test
+    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() =
+        testScope.runTest {
+            val taskId = 2
+            val expectedThumbnailData = createThumbnailData()
+            tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+            tasksRepository.seedTasks(tasks)
+
+            systemUnderTest.bind(taskId)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
+
+            tasksRepository.setVisibleTasks(setOf(taskId))
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(
+                    SnapshotSplash(
+                        Snapshot(
+                            backgroundColor = Color.rgb(2, 2, 2),
+                            bitmap = expectedThumbnailData.thumbnail!!,
+                            thumbnailRotation = Surface.ROTATION_0,
+                        ),
+                        expectedIconData,
+                    )
+                )
+        }
+
+    @Test
+    fun getSnapshotMatrix_MissingThumbnail() =
+        testScope.runTest {
+            val taskId = 2
+            val isRtl = true
+
+            whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+                .thenReturn(MissingThumbnail)
+
+            systemUnderTest.bind(taskId)
+            assertThat(
+                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+                )
+                .isEqualTo(Matrix.IDENTITY_MATRIX)
+        }
+
+    @Test
+    fun getSnapshotMatrix_MatrixScaling() =
+        testScope.runTest {
+            val taskId = 2
+            val isRtl = true
+
+            whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+                .thenReturn(MatrixScaling(MATRIX, isRotated = false))
+
+            systemUnderTest.bind(taskId)
+            assertThat(
+                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+                )
+                .isEqualTo(MATRIX)
+        }
+
+    @Test
+    fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() =
+        testScope.runTest {
+            recentsViewData.tintAmount.value = 0.32f
+            taskContainerData.taskMenuOpenProgress.value = 0f
+            assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
+        }
+
+    @Test
+    fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() =
+        testScope.runTest {
+            recentsViewData.tintAmount.value = 0f
+            taskContainerData.taskMenuOpenProgress.value = 1f
+            assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
+        }
+
+    @Test
+    fun getForegroundScrimDimProgress_returnsNoScrim() =
+        testScope.runTest {
+            recentsViewData.tintAmount.value = 0f
+            taskContainerData.taskMenuOpenProgress.value = 0f
+            assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
+        }
+
+    private fun createTaskWithId(taskId: Int) =
+        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+        }
+
+    private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
+        val bitmap = mock<Bitmap>()
+        whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
+        whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
+
+        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+    }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+        const val CANVAS_WIDTH = 300
+        const val CANVAS_HEIGHT = 600
+        val MATRIX =
+            Matrix().apply {
+                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+            }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
deleted file mode 100644
index b78f871..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.thumbnail
-
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.Rect
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.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.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.quickstep.task.viewmodel.TaskViewData
-import com.android.quickstep.views.TaskViewType
-import com.android.systemui.shared.recents.model.Task
-import com.android.systemui.shared.recents.model.ThumbnailData
-import com.google.common.truth.Truth.assertThat
-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
-
-@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 systemUnderTest by lazy {
-        TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
-    }
-
-    private val tasks = (0..5).map(::createTaskWithId)
-
-    @Test
-    fun initialStateIsUninitialized() = runTest {
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
-    }
-
-    @Test
-    fun bindRunningTask_thenStateIs_LiveTile() = runTest {
-        tasksRepository.seedTasks(tasks)
-        val taskThumbnail = TaskThumbnail(taskId = 1, isRunning = true)
-        systemUnderTest.bind(taskThumbnail)
-
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
-    }
-
-    @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 {
-            tasksRepository.seedTasks(tasks)
-            val runningTask = TaskThumbnail(taskId = 1, isRunning = true)
-            val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
-            systemUnderTest.bind(runningTask)
-            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
-
-            systemUnderTest.bind(stoppedTask)
-            assertThat(systemUnderTest.uiState.first())
-                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
-        }
-
-    @Test
-    fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
-        tasksRepository.seedTasks(tasks)
-        val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
-
-        systemUnderTest.bind(stoppedTask)
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
-    }
-
-    @Test
-    fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest {
-        tasksRepository.seedThumbnailData(mapOf(2 to createThumbnailData()))
-        tasks[2].isLocked = true
-        tasksRepository.seedTasks(tasks)
-        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
-
-        systemUnderTest.bind(recentTask)
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
-    }
-
-    @Test
-    fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest {
-        val expectedThumbnailData = createThumbnailData()
-        tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(2))
-        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
-
-        systemUnderTest.bind(recentTask)
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(
-                Snapshot(
-                    backgroundColor = Color.rgb(2, 2, 2),
-                    bitmap = expectedThumbnailData.thumbnail!!,
-                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
-                )
-            )
-    }
-
-    @Test
-    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest {
-        val expectedThumbnailData = createThumbnailData()
-        tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
-        tasksRepository.seedTasks(tasks)
-        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
-
-        systemUnderTest.bind(recentTask)
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
-        tasksRepository.setVisibleTasks(listOf(2))
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(
-                Snapshot(
-                    backgroundColor = Color.rgb(2, 2, 2),
-                    bitmap = expectedThumbnailData.thumbnail!!,
-                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
-                )
-            )
-    }
-
-    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(): ThumbnailData {
-        val bitmap = mock<Bitmap>()
-        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/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index 40482c4..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
@@ -23,9 +23,13 @@
 import android.graphics.Matrix
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel.ThumbnailPositionState
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
@@ -53,7 +57,9 @@
         )
     private val recentsViewData = RecentsViewData()
     private val tasksRepository = FakeTasksRepository()
-    private val systemUnderTest = TaskOverlayViewModel(task, recentsViewData, tasksRepository)
+    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+    private val systemUnderTest =
+        TaskOverlayViewModel(task, recentsViewData, mGetThumbnailPositionUseCase, tasksRepository)
 
     @Test
     fun initialStateIsDisabled() = runTest {
@@ -83,13 +89,7 @@
         task.isLocked = false
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = false,
-                    thumbnail = null,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
     }
 
     @Test
@@ -98,18 +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,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test
@@ -118,18 +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,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test
@@ -138,23 +126,46 @@
         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,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
+    }
+
+    @Test
+    fun getThumbnailMatrix_MissingThumbnail() = runTest {
+        val isRtl = true
+
+        whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .thenReturn(MissingThumbnail)
+
+        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
+    }
+
+    @Test
+    fun getThumbnailMatrix_MatrixScaling() = runTest {
+        val isRtl = true
+        val isRotated = true
+
+        whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .thenReturn(MatrixScaling(MATRIX, isRotated))
+
+        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
     }
 
     companion object {
         const val TASK_ID = 0
         const val THUMBNAIL_WIDTH = 100
         const val THUMBNAIL_HEIGHT = 200
+        const val CANVAS_WIDTH = 300
+        const val CANVAS_HEIGHT = 600
+        val MATRIX =
+            Matrix().apply {
+                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+            }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
index 0bf68eb..d66197a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.taskbar.customization
 
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator
 import com.android.launcher3.taskbar.customization.TaskbarIconSpecs
 import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator
@@ -32,7 +33,9 @@
 class TaskbarSpecsEvaluatorTest {
 
     private val taskbarFeatureEvaluator = mock<TaskbarFeatureEvaluator>()
-    private val taskbarSpecsEvaluator = spy(TaskbarSpecsEvaluator(taskbarFeatureEvaluator))
+    private val taskbarActivityContext = mock<TaskbarActivityContext>()
+    private var taskbarSpecsEvaluator =
+        spy(TaskbarSpecsEvaluator(taskbarActivityContext, taskbarFeatureEvaluator, 0, 0))
 
     @Test
     fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumnInLandscape() {
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 ece67af..541a48d 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.common.split.SplitScreenConstants.SNAP_TO_30_70
-import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50
-import com.android.wm.shell.common.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),
         )
     }
 
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 f11cd0b..108cfb5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -23,7 +23,7 @@
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
-import com.android.wm.shell.common.split.SplitScreenConstants
+import com.android.wm.shell.shared.split.SplitScreenConstants
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -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/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index f3cde52..5051251 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -27,6 +27,7 @@
 import android.window.TransitionInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.apppairs.AppPairIcon
+import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.taskbar.TaskbarActivityContext
@@ -76,6 +77,7 @@
     private val splitSelectSource: SplitConfigurationOptions.SplitSelectSource = mock()
     private val mockSplitSourceDrawable: Drawable = mock()
     private val mockSplitSourceView: View = mock()
+    private val mockItemInfo: ItemInfo = mock()
 
     private val stateManager: StateManager<*, *> = mock()
     private val depthController: DepthController = mock()
@@ -89,11 +91,13 @@
         whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView)
         whenever(mockTaskContainer.splitAnimationThumbnail).thenReturn(mockBitmap)
         whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
+        whenever(mockTaskContainer.task).thenReturn(mockTask)
         whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable)
         whenever(mockTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer })
 
         whenever(splitSelectSource.drawable).thenReturn(mockSplitSourceDrawable)
         whenever(splitSelectSource.view).thenReturn(mockSplitSourceView)
+        whenever(splitSelectSource.itemInfo).thenReturn(mockItemInfo)
 
         splitAnimationController = SplitAnimationController(mockSplitSelectStateController)
     }
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 bab84ef..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.common.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/TaskGridNavHelperTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java
deleted file mode 100644
index 7ef4910..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java
+++ /dev/null
@@ -1,510 +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.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
-import static com.android.quickstep.util.TaskGridNavHelper.INVALID_FOCUSED_TASK_ID;
-
-import static org.junit.Assert.assertEquals;
-
-import com.android.launcher3.util.IntArray;
-
-import org.junit.Test;
-
-public class TaskGridNavHelperTest {
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 1;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 2, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 1;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 2, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 2;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 1, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 2;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 1, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 1;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 3, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 2;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 4, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 3;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 1, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 4;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 2, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 1;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 2;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 5;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 6;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 1, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 6, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_withFocused_onFocused_pressLeft_toTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int focusedTaskId = 99;
-        int currentPageTaskViewId = focusedTaskId;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 1, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int focusedTaskId = 99;
-        int currentPageTaskViewId = focusedTaskId;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int focusedTaskId = 99;
-        int currentPageTaskViewId = focusedTaskId;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int focusedTaskId = 99;
-        int currentPageTaskViewId = focusedTaskId;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int focusedTaskId = 99;
-        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
-    }
-
-    @Test
-    public void longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 7;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 7, nextGridPage);
-    }
-
-    @Test
-    public void longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 7;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 7, nextGridPage);
-    }
-
-    @Test
-    public void longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 6;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 7, nextGridPage);
-    }
-
-    @Test
-    public void longerTopRow_noFocused_atClearAll_pressRight_goToLonger() {
-        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 7, nextGridPage);
-    }
-
-    @Test
-    public void longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6, 7);
-        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 7, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 1;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 2, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 2;
-        int delta = 1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 3, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 3;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 2, nextGridPage);
-    }
-
-    @Test
-    public void equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
-        IntArray topIds = IntArray.wrap(1, 3, 5);
-        IntArray bottomIds = IntArray.wrap(2, 4, 6);
-        int currentPageTaskViewId = 2;
-        int delta = -1;
-        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
-        boolean cycle = true;
-        TaskGridNavHelper taskGridNavHelper =
-                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
-
-        int nextGridPage =
-                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-
-        assertEquals("Wrong next page returned.", 1, nextGridPage);
-    }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
new file mode 100644
index 0000000..7aab75f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -0,0 +1,638 @@
+/*
+ * 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 com.android.launcher3.util.IntArray
+import com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN
+import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT
+import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT
+import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB
+import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class TaskGridNavHelperTest {
+
+    /*
+                    5   3   1
+        CLEAR_ALL           ↓
+                    6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_DOWN, delta = 1))
+            .isEqualTo(2)
+    }
+
+    /*                      ↑----→
+                    5   3   1    |
+        CLEAR_ALL                |
+                    6   4   2←---|
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_UP, delta = 1)).isEqualTo(2)
+    }
+
+    /*                      ↓----↑
+                    5   3   1    |
+        CLEAR_ALL                |
+                    6   4   2    |
+                            ↓----→
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_DOWN, delta = 1))
+            .isEqualTo(1)
+    }
+
+    /*
+                    5   3   1
+        CLEAR_ALL           ↑
+                    6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_UP, delta = 1)).isEqualTo(1)
+    }
+
+    /*
+                    5   3<--1
+        CLEAR_ALL
+                    6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_LEFT, delta = 1))
+            .isEqualTo(3)
+    }
+
+    /*
+                    5   3   1
+        CLEAR_ALL
+                    6   4<--2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_LEFT, delta = 1))
+            .isEqualTo(4)
+    }
+
+    /*
+                    5   3-->1
+        CLEAR_ALL
+                    6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_RIGHT, delta = -1))
+            .isEqualTo(1)
+    }
+
+    /*
+                    5   3   1
+        CLEAR_ALL
+                    6   4-->2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 4, DIRECTION_RIGHT, delta = -1))
+            .isEqualTo(2)
+    }
+
+    /*
+             ↓------------------←
+             |                  |
+             ↓      5   3   1---→
+        CLEAR_ALL
+                    6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_RIGHT, delta = -1))
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
+             ↓------------------←
+             |                  ↑
+             ↓      5   3   1   |
+        CLEAR_ALL               ↑
+                    6   4   2---→
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_RIGHT, delta = -1))
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
+              ←----5   3   1
+              ↓
+        CLEAR_ALL
+                   6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 5, DIRECTION_LEFT, delta = 1))
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
+                   5   3   1
+        CLEAR_ALL
+               ↑
+               ←---6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 6, DIRECTION_LEFT, delta = 1))
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
+       |→-----------------------|
+       |                        ↓
+       ↑                5   3   1
+       ←------CLEAR_ALL
+
+                        6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                )
+            )
+            .isEqualTo(1)
+    }
+
+    /*
+                       5   3   1
+        CLEAR_ALL--↓
+                   |
+                   |--→6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                )
+            )
+            .isEqualTo(6)
+    }
+
+    /*
+                    5   3   1←---
+                                 ↑
+        CLEAR_ALL                ←--FOCUSED_TASK
+                    6   4   2
+    */
+    @Test
+    fun equalLengthRows_withFocused_onFocused_pressLeft_toTop() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = FOCUSED_TASK_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    largeTileIds = listOf(FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(1)
+    }
+
+    /*
+                        5   3   1
+                                   ←--↑
+        CLEAR_ALL                  ↓-→FOCUSED_TASK
+                        6   4   2
+    */
+    @Test
+    fun equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = FOCUSED_TASK_ID,
+                    DIRECTION_UP,
+                    delta = 1,
+                    largeTileIds = listOf(FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(FOCUSED_TASK_ID)
+    }
+
+    /*
+                        5   3   1
+        CLEAR_ALL                  ↑--→FOCUSED_TASK
+                                   ↑←--↓
+                        6   4   2
+    */
+
+    @Test
+    fun equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() {
+
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = FOCUSED_TASK_ID,
+                    DIRECTION_DOWN,
+                    delta = 1,
+                    largeTileIds = listOf(FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(FOCUSED_TASK_ID)
+    }
+
+    /*
+             ↓-------------------------------←|
+             |                                ↑
+             ↓      5   3   1                 |
+        CLEAR_ALL               FOCUSED_TASK--→
+                    6   4   2
+    */
+    @Test
+    fun equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() {
+
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = FOCUSED_TASK_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    largeTileIds = listOf(FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
+           |→---------------------------|
+           |                            |
+           ↑                5   3   1   ↓
+           ←------CLEAR_ALL            FOCUSED_TASK
+
+                            6   4   2
+    */
+    @Test
+    fun equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() {
+
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    largeTileIds = listOf(FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(FOCUSED_TASK_ID)
+    }
+
+    /*
+                         7←-↑  5   3   1
+                         ↓--→
+           CLEAR_ALL
+                               6   4   2
+    */
+    @Test
+    fun longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = 7,
+                    DIRECTION_DOWN,
+                    delta = 1,
+                    topIds = IntArray.wrap(1, 3, 5, 7),
+                )
+            )
+            .isEqualTo(7)
+    }
+
+    /*
+                       ←--↑
+                       ↓-→7   5   3   1
+           CLEAR_ALL
+                              6   4   2
+    */
+    @Test
+    fun longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() {
+        assertThat(
+                getNextGridPage(
+                    /* topIds = */ currentPageTaskViewId = 7,
+                    DIRECTION_UP,
+                    delta = 1,
+                    topIds = IntArray.wrap(1, 3, 5, 7),
+                )
+            )
+            .isEqualTo(7)
+    }
+
+    /*
+                         7   5   3   1
+           CLEAR_ALL     ↑
+                         ←----6   4   2
+    */
+    @Test
+    fun longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() {
+        assertThat(
+                getNextGridPage(
+                    /* topIds = */ currentPageTaskViewId = 6,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    topIds = IntArray.wrap(1, 3, 5, 7),
+                )
+            )
+            .isEqualTo(7)
+    }
+
+    /*
+                         7   5   3   1
+                         ↑
+           CLEAR_ALL-----→
+                             6   4   2
+    */
+    @Test
+    fun longerTopRow_noFocused_atClearAll_pressRight_goToLonger() {
+        assertThat(
+                getNextGridPage(
+                    /* topIds = */ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    topIds = IntArray.wrap(1, 3, 5, 7),
+                )
+            )
+            .isEqualTo(7)
+    }
+
+    /*
+                            5   3   1
+           CLEAR_ALL-----→
+                         ↓
+                         7  6   4   2
+    */
+    @Test
+    fun longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    bottomIds = IntArray.wrap(2, 4, 6, 7),
+                )
+            )
+            .isEqualTo(7)
+    }
+
+    /*
+                   5   3   1
+        CLEAR_ALL          ↓
+                   6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = 1))
+            .isEqualTo(2)
+    }
+
+    /*
+                   5   3   1
+        CLEAR_ALL      ↑
+                       ←---↑
+                   6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = 1))
+            .isEqualTo(3)
+    }
+
+    /*
+                   5   3   1
+        CLEAR_ALL      ↓
+                       ----→
+                           ↓
+                   6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_TAB, delta = -1))
+            .isEqualTo(2)
+    }
+
+    /*
+                   5   3   1
+        CLEAR_ALL          ↑
+                   6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = -1))
+            .isEqualTo(1)
+    }
+
+    /*
+                        5   3   1
+           CLEAR_ALL                FOCUSED_TASK←--DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withLargeTile_pressLeftFromDesktopTask_goesToFocusedTask() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = DESKTOP_TASK_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(FOCUSED_TASK_ID)
+    }
+
+    /*
+                        5   3   1
+           CLEAR_ALL                FOCUSED_TASK--→DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withLargeTile_pressRightFromFocusedTask_goesToDesktopTask() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = FOCUSED_TASK_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(DESKTOP_TASK_ID)
+    }
+
+    /*
+             ↓-----------------------------------------←|
+             |                                          |
+             ↓      5   3   1                           ↑
+        CLEAR_ALL               FOCUSED_TASK   DESKTOP--→
+                    6   4   2
+    */
+    @Test
+    fun withLargeTile_pressRightFromDesktopTask_goesToClearAll() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = DESKTOP_TASK_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
+           |→-------------------------------------------|
+           |                                            |
+           ↑                5   3   1                   ↓
+           ←------CLEAR_ALL             FOCUSED_TASK   DESKTOP
+
+                            6   4   2
+    */
+    @Test
+    fun withLargeTile_pressLeftFromClearAll_goesToDesktopTask() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(DESKTOP_TASK_ID)
+    }
+
+    /*
+                    5   3   1
+       CLEAR_ALL                 FOCUSED_TASK   DESKTOP
+                                  ↑
+                    6   4   2→----↑
+    */
+    @Test
+    fun withLargeTile_pressRightFromBottom_goesToLargeTile() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = 2,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(FOCUSED_TASK_ID)
+    }
+
+    /*
+                        5   3   1→----|
+                                      ↓
+         CLEAR_ALL                    FOCUSED_TASK   DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withLargeTile_pressRightFromTop_goesToLargeTile() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = 1,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(FOCUSED_TASK_ID)
+    }
+
+    /*
+                        5   3   1
+
+         CLEAR_ALL                FOCUSED_TASK←---DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withLargeTile_pressTabFromDeskTop_goesToFocusedTask() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = DESKTOP_TASK_ID,
+                    DIRECTION_TAB,
+                    delta = 1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(FOCUSED_TASK_ID)
+    }
+
+    /*
+        CLEAR_ALL         FOCUSED_TASK   DESKTOP
+                           ↓
+                     2←----↓
+    */
+    @Test
+    fun withLargeTile_pressLeftFromLargeTile_goesToBottom() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = FOCUSED_TASK_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    topIds = IntArray(),
+                    bottomIds = IntArray.wrap(2),
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(2)
+    }
+
+    /*
+             ↓-----------------------------------------←|
+             |                                          |
+             ↓      5   3   1                           ↑
+        CLEAR_ALL               FOCUSED_TASK   DESKTOP--→
+                    6   4   2
+    */
+    @Test
+    fun withLargeTile_pressShiftTabFromDeskTop_goesToClearAll() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = DESKTOP_TASK_ID,
+                    DIRECTION_TAB,
+                    delta = -1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
+                )
+            )
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    private fun getNextGridPage(
+        currentPageTaskViewId: Int,
+        direction: Int,
+        delta: Int,
+        topIds: IntArray = IntArray.wrap(1, 3, 5),
+        bottomIds: IntArray = IntArray.wrap(2, 4, 6),
+        largeTileIds: List<Int> = emptyList(),
+    ): Int {
+        val taskGridNavHelper = TaskGridNavHelper(topIds, bottomIds, largeTileIds)
+        return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, true)
+    }
+
+    private companion object {
+        const val FOCUSED_TASK_ID = 99
+        const val DESKTOP_TASK_ID = 100
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt
new file mode 100644
index 0000000..6c526a4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.util.SafeCloseable
+import com.android.quickstep.DeviceConfigWrapper.Companion.configHelper
+import com.android.quickstep.util.DeviceConfigHelper.Companion.prefs
+import java.util.concurrent.CountDownLatch
+import java.util.function.BooleanSupplier
+import org.junit.Assert
+import org.junit.Assume
+
+/** Helper methods for testing */
+object TestExtensions {
+
+    @JvmStatic
+    fun overrideNavConfigFlag(
+        key: String,
+        value: Boolean,
+        targetValue: BooleanSupplier
+    ): AutoCloseable {
+        Assume.assumeTrue(BuildConfig.IS_DEBUG_DEVICE)
+        if (targetValue.asBoolean == value) {
+            return AutoCloseable {}
+        }
+
+        navConfigEditWatcher().let {
+            prefs.edit().putBoolean(key, value).commit()
+            it.close()
+        }
+        Assert.assertEquals(value, targetValue.asBoolean)
+
+        val watcher = navConfigEditWatcher()
+        return AutoCloseable {
+            prefs.edit().remove(key).commit()
+            watcher.close()
+        }
+    }
+
+    private fun navConfigEditWatcher(): SafeCloseable {
+        val wait = CountDownLatch(1)
+        val listener = Runnable { wait.countDown() }
+        configHelper.addChangeListener(listener)
+
+        return SafeCloseable {
+            wait.await()
+            configHelper.removeChangeListener(listener)
+        }
+    }
+}
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/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
index 15b1e53..d064f4a 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
@@ -57,6 +57,7 @@
     @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
     @Mock lateinit var taskbarPinningController: TaskbarPinningController
     @Mock lateinit var optionalBubbleControllers: Optional<BubbleControllers>
+    @Mock lateinit var taskbarDesktopModeController: TaskbarDesktopModeController
 
     lateinit var taskbarControllers: TaskbarControllers
 
@@ -98,6 +99,7 @@
                 keyboardQuickSwitchController,
                 taskbarPinningController,
                 optionalBubbleControllers,
+                taskbarDesktopModeController
             )
     }
 }
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
index ef3a833..67a0ee4 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -202,7 +202,7 @@
         boolean hoverHandled =
                 mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
 
-        assertThat(hoverHandled).isFalse();
+        assertThat(hoverHandled).isTrue();
     }
 
     @Test
@@ -213,7 +213,7 @@
         boolean hoverHandled =
                 mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
 
-        assertThat(hoverHandled).isFalse();
+        assertThat(hoverHandled).isTrue();
     }
 
     @Test
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 27e761a..066ddc0 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -18,16 +18,21 @@
 
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.ComponentName
+import android.content.Context
 import android.content.Intent
+import android.content.res.Resources
 import android.os.Process
 import android.os.UserHandle
+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.statehandlers.DesktopVisibilityController
+import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RecentsModel.RecentTasksChangedListener
 import com.android.quickstep.TaskIconCache
@@ -39,12 +44,14 @@
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.runner.Description
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.never
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -53,32 +60,62 @@
 class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
 
     @get:Rule val mockitoRule = MockitoJUnit.rule()
+    @get:Rule
+    val disableControllerForCertainTestsWatcher =
+        object : TestWatcher() {
+            override fun starting(description: Description) {
+                // Update canShowRunningAndRecentAppsAtInit before setUp() is called for each test.
+                canShowRunningAndRecentAppsAtInit =
+                    description.methodName !in
+                        listOf("canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled")
+            }
+        }
 
     @Mock private lateinit var mockIconCache: TaskIconCache
     @Mock private lateinit var mockRecentsModel: RecentsModel
-    @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
+    @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var mockResources: Resources
 
     private var taskListChangeId: Int = 1
 
     private lateinit var recentAppsController: TaskbarRecentAppsController
-    private lateinit var recentTasksChangedListener: RecentTasksChangedListener
-    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
 
     @Before
     fun setUp() {
         super.setup()
-        userHandle = Process.myUserHandle()
+        myUserHandle = Process.myUserHandle()
+
+        // Set desktop mode supported
+        whenever(mockContext.getResources()).thenReturn(mockResources)
+        whenever(mockResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true)
 
         whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
-        recentAppsController =
-            TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
+        whenever(mockRecentsModel.unregisterRecentTasksChangedListener()).then {
+            recentTasksChangedListener = null
+            it
+        }
+        recentAppsController = TaskbarRecentAppsController(mockContext, mockRecentsModel)
+        recentAppsController.canShowRunningApps = canShowRunningAndRecentAppsAtInit
+        recentAppsController.canShowRecentApps = canShowRunningAndRecentAppsAtInit
         recentAppsController.init(taskbarControllers)
-        recentAppsController.canShowRunningApps = true
-        recentAppsController.canShowRecentApps = true
+        taskbarControllers.onPostInit()
 
-        val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
-        verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
-        recentTasksChangedListener = listenerCaptor.value
+        recentTasksChangedListener =
+            if (canShowRunningAndRecentAppsAtInit) {
+                val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
+                verify(mockRecentsModel)
+                    .registerRecentTasksChangedListener(listenerCaptor.capture())
+                listenerCaptor.value
+            } else {
+                verify(mockRecentsModel, never()).registerRecentTasksChangedListener(any())
+                null
+            }
 
         // Make sure updateHotseatItemInfos() is called after commitRunningAppsToUI()
         whenever(taskbarViewController.commitRunningAppsToUI()).then {
@@ -88,6 +125,141 @@
         }
     }
 
+    // See the TestWatcher rule at the top which sets canShowRunningAndRecentAppsAtInit = false.
+    @Test
+    fun canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled() {
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+            runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+        )
+        verify(mockRecentsModel, never()).getTasks(any<Consumer<List<GroupTask>>>())
+    }
+
+    @Test
+    fun canShowRunningAndRecentAppsIsFalseAfterInit_getTasksOnlyCalledInInit() {
+        // getTasks() should have been called once from init().
+        verify(mockRecentsModel, times(1)).getTasks(any<Consumer<List<GroupTask>>>())
+        recentAppsController.canShowRunningApps = false
+        recentAppsController.canShowRecentApps = false
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+            runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+        )
+        // Verify that getTasks() was not called again after the init().
+        verify(mockRecentsModel, times(1)).getTasks(any<Consumer<List<GroupTask>>>())
+    }
+
+    @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
@@ -97,7 +269,7 @@
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
                 runningTasks = emptyList(),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
             .containsExactlyElementsIn(hotseatPackages)
@@ -112,7 +284,7 @@
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
                 runningTasks = emptyList(),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
             .containsExactlyElementsIn(hotseatPackages)
@@ -126,7 +298,7 @@
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
                 runningTasks = emptyList(),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
         val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
         assertThat(newHotseatItems.map { it?.targetPackage })
@@ -141,7 +313,7 @@
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
                 runningTasks = listOf(createTask(id = 1, HOTSEAT_PACKAGE_1)),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
 
         assertThat(newHotseatItems).hasLength(2)
@@ -161,9 +333,9 @@
                 runningTasks =
                     listOf(
                         createTask(id = 1, HOTSEAT_PACKAGE_1),
-                        createTask(id = 2, HOTSEAT_PACKAGE_1)
+                        createTask(id = 2, HOTSEAT_PACKAGE_1),
                     ),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
 
         // First task is in Hotseat Items
@@ -186,7 +358,7 @@
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
                 runningTasks = emptyList(),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
         val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
         assertThat(newHotseatItems.map { it?.targetPackage })
@@ -202,9 +374,9 @@
             runningTasks =
                 listOf(
                     createTask(id = 1, RUNNING_APP_PACKAGE_1),
-                    createTask(id = 2, RUNNING_APP_PACKAGE_2)
+                    createTask(id = 2, RUNNING_APP_PACKAGE_2),
                 ),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
     }
@@ -216,7 +388,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
     }
@@ -229,9 +401,9 @@
             runningTasks =
                 listOf(
                     createTask(id = 1, RUNNING_APP_PACKAGE_1),
-                    createTask(id = 2, RUNNING_APP_PACKAGE_2)
+                    createTask(id = 2, RUNNING_APP_PACKAGE_2),
                 ),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
         assertThat(recentAppsController.minimizedTaskIds).isEmpty()
@@ -243,7 +415,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
     }
@@ -256,7 +428,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         val shownTasks = recentAppsController.shownTasks.map { it.task1 }
         assertThat(shownTasks).containsExactlyElementsIn(listOf(task1, task2))
@@ -270,7 +442,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).isEmpty()
         assertThat(recentAppsController.minimizedTaskIds).isEmpty()
@@ -284,7 +456,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
         assertThat(recentAppsController.minimizedTaskIds).isEmpty()
@@ -297,12 +469,12 @@
             listOf(
                 createTask(id = 1, HOTSEAT_PACKAGE_1),
                 createTask(id = 2, RUNNING_APP_PACKAGE_1),
-                createTask(id = 3, RUNNING_APP_PACKAGE_2)
+                createTask(id = 3, RUNNING_APP_PACKAGE_2),
             )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
             runningTasks = runningTasks,
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2, 3))
         assertThat(recentAppsController.minimizedTaskIds).isEmpty()
@@ -318,7 +490,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = runningTasks,
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).containsExactly(1, 2, 3)
         assertThat(recentAppsController.minimizedTaskIds).containsExactly(3)
@@ -332,7 +504,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
     }
@@ -345,13 +517,13 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task2, task1),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         val shownTasks = recentAppsController.shownTasks.map { it.task1 }
@@ -366,13 +538,13 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task2, task1),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         val shownTasks = recentAppsController.shownTasks.map { it.task1 }
@@ -387,17 +559,17 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         updateRecentTasks( // Trigger a recent-tasks change before calling updateHotseatItems()
             runningTasks = listOf(task2, task1),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
             runningTasks = listOf(task2, task1),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         val newHotseatItems = recentAppsController.shownHotseatItems
@@ -414,12 +586,12 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3),
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+            recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1),
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
@@ -435,12 +607,12 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task2, task1, task3),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         val expectedOrder =
@@ -454,12 +626,12 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2),
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+            recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1),
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
@@ -475,12 +647,12 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2, task3),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task2, task1),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         assertThat(shownPackages).isEqualTo(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
@@ -492,12 +664,12 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3),
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+            recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3),
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         // Most recent packages, minus the currently running one (RECENT_PACKAGE_3).
@@ -514,11 +686,11 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(runningTask1, runningTask2),
-            recentTaskPackages = recentTaskPackages
+            recentTaskPackages = recentTaskPackages,
         )
 
         setInDesktopMode(true)
-        recentTasksChangedListener.onRecentTasksChanged()
+        recentTasksChangedListener!!.onRecentTasksChanged()
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         assertThat(shownPackages).containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
     }
@@ -532,10 +704,10 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(runningTask1, runningTask2),
-            recentTaskPackages = recentTaskPackages
+            recentTaskPackages = recentTaskPackages,
         )
         setInDesktopMode(false)
-        recentTasksChangedListener.onRecentTasksChanged()
+        recentTasksChangedListener!!.onRecentTasksChanged()
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         // Don't expect RECENT_PACKAGE_3 because it is currently running.
         val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
@@ -548,7 +720,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3),
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         // RECENT_PACKAGE_3 is the top task (visible to user) so should be excluded.
@@ -564,7 +736,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(runningTask1, runningTask2),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
         // Only 2 recent tasks shown: Desktop Tile + 1 Recent Task
@@ -580,7 +752,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
         // Only 2 recent tasks shown: Pair + 1 Recent Task
@@ -596,14 +768,14 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
         // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = emptyList(),
-            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
     }
@@ -616,14 +788,14 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(runningTask1, runningTask2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
         // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(runningTask1, runningTask2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
     }
@@ -637,7 +809,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1Minimized, task2Visible),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
 
@@ -645,7 +817,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1Minimized, task2Minimized),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         verify(taskbarViewController, times(2)).commitRunningAppsToUI()
@@ -661,7 +833,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = hotseatPackages,
             runningTasks = originalTasks,
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
 
@@ -669,7 +841,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = hotseatPackages,
             runningTasks = newTasks,
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         verify(taskbarViewController, times(2)).commitRunningAppsToUI()
@@ -686,10 +858,7 @@
         return recentAppsController.shownHotseatItems.toTypedArray()
     }
 
-    private fun updateRecentTasks(
-        runningTasks: List<Task>,
-        recentTaskPackages: List<String>,
-    ) {
+    private fun updateRecentTasks(runningTasks: List<Task>, recentTaskPackages: List<String>) {
         val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
         val allTasks =
             ArrayList<GroupTask>().apply {
@@ -705,7 +874,7 @@
             }
             .whenever(mockRecentsModel)
             .getTasks(any<Consumer<List<GroupTask>>>())
-        recentTasksChangedListener.onRecentTasksChanged()
+        recentTasksChangedListener?.onRecentTasksChanged()
     }
 
     private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
@@ -725,8 +894,14 @@
 
     private fun createTestAppInfo(
         packageName: String = "testPackageName",
-        className: String = "testClassName"
-    ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
+        className: String = "testClassName",
+    ) =
+        AppInfo(
+            ComponentName(packageName, className),
+            className /* title */,
+            myUserHandle,
+            Intent(),
+        )
 
     private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
         return packageNames.map { packageName ->
@@ -735,7 +910,7 @@
                 GroupTask(
                     createTask(100, splitPackages[0]),
                     createTask(101, splitPackages[1]),
-                    /* splitBounds = */ null
+                    /* splitBounds = */ null,
                 )
             } else {
                 // Use the number at the end of the test packageName as the id.
@@ -745,22 +920,38 @@
         }
     }
 
-    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,
-                    0
+                    localUserHandle?.identifier ?: myUserHandle.identifier,
+                    0,
                 )
             )
             .apply { this.isVisible = isVisible }
     }
 
     private fun setInDesktopMode(inDesktopMode: Boolean) {
-        whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
+        whenever(taskbarControllers.taskbarDesktopModeController.areDesktopTasksVisible)
+            .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>
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 d9d5585..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
@@ -39,8 +41,8 @@
 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.common.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -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 70%
rename from quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
rename to quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index 6e25b10..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,41 +46,42 @@
 
 @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.hasLimit());
-            assertEquals("Toast text: ", "5 minutes left today", toast.getText());
+            waitForLauncherCondition("Toast is not visible", launcher -> toast.getHasLimit());
+            assertEquals("Toast text: ", "5 minutes left today", toast.getBannerText());
 
             // Unset time limit for app.
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
 
-            mLauncher.goHome();
-            assertFalse("Toast is visible", getToast().hasLimit());
+            goToState(LauncherState.NORMAL);
+            assertFalse("Toast is visible", getToast().getHasLimit());
         } finally {
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
@@ -83,12 +89,12 @@
     }
 
     private DigitalWellBeingToast getToast() {
-        mLauncher.getWorkspace().switchToOverview();
+        goToState(LauncherState.OVERVIEW);
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
             TaskContainer taskContainer = task.getTaskContainers().get(0);
-            assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals(
+            assertTrue("Latest task is not Calculator", calculatorPackage.equals(
                     taskContainer.getTask().getTopComponent().getPackageName()));
             return taskContainer.getDigitalWellBeingToast();
         });
@@ -105,6 +111,5 @@
         } finally {
             getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
         }
-
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
new file mode 100644
index 0000000..8968b9c
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.content.ComponentName
+import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.AbstractFloatingViewHelper
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.uioverrides.QuickstepLauncher
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.TransformingTouchDelegate
+import com.android.quickstep.TaskOverlayFactory.TaskOverlay
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.views.LauncherRecentsView
+import com.android.quickstep.views.TaskContainer
+import com.android.quickstep.views.TaskThumbnailViewDeprecated
+import com.android.quickstep.views.TaskView
+import com.android.quickstep.views.TaskViewIcon
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.window.flags.Flags
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+/** Test for ExternalDisplaySystemShortcut */
+class ExternalDisplaySystemShortcutTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
+
+    private val launcher: QuickstepLauncher = mock()
+    private val statsLogManager: StatsLogManager = mock()
+    private val statsLogger: StatsLogManager.StatsLogger = mock()
+    private val recentsView: LauncherRecentsView = mock()
+    private val taskView: TaskView = mock()
+    private val workspaceItemInfo: WorkspaceItemInfo = mock()
+    private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock()
+    private val iconView: TaskViewIcon = mock()
+    private val transformingTouchDelegate: TransformingTouchDelegate = mock()
+    private val factory: TaskShortcutFactory =
+        ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper)
+    private val overlayFactory: TaskOverlayFactory = mock()
+    private val overlay: TaskOverlay<*> = mock()
+
+    private lateinit var mockitoSession: StaticMockitoSession
+
+    @Before
+    fun setUp() {
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .startMocking()
+        ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() }
+        ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        whenever(overlayFactory.createOverlay(any())).thenReturn(overlay)
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT)
+    fun createExternalDisplayTaskShortcut_desktopModeDisabled() {
+        val task = createTask()
+        val taskContainer = createTaskContainer(task)
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).isNull()
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+        Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
+    )
+    fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported() {
+        ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+        val taskContainer = createTaskContainer(createTask())
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).isNull()
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+        Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
+    )
+    fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported_overrideEnabled() {
+        ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() }
+
+        val taskContainer = spy(createTaskContainer(createTask()))
+        doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).isNotNull()
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+        Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
+    )
+    fun externalDisplaySystemShortcutClicked() {
+        val task = createTask()
+        val taskContainer = spy(createTaskContainer(task))
+
+        whenever(launcher.getOverviewPanel<LauncherRecentsView>()).thenReturn(recentsView)
+        whenever(launcher.statsLogManager).thenReturn(statsLogManager)
+        whenever(statsLogManager.logger()).thenReturn(statsLogger)
+        whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
+        whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer {
+            val successCallback = it.getArgument<Runnable>(1)
+            successCallback.run()
+        }
+        doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).hasSize(1)
+        assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java)
+
+        val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut
+
+        externalDisplayShortcut.onClick(taskView)
+
+        val allTypesExceptRebindSafe =
+            AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
+        verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe)
+        verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any())
+        verify(statsLogger).withItemInfo(workspaceItemInfo)
+        verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP)
+    }
+
+    private fun createTask(): Task = Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000))
+
+    private fun createTaskContainer(task: Task): TaskContainer {
+        val snapshotView =
+            if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
+            else mock<TaskThumbnailViewDeprecated>()
+        return TaskContainer(
+            taskView,
+            task,
+            snapshotView,
+            iconView,
+            transformingTouchDelegate,
+            SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+            digitalWellBeingToast = null,
+            showWindowsView = null,
+            overlayFactory,
+        )
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 2858929..aa105f9 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -22,9 +22,7 @@
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage;
 import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
-import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_ACTIVITY_TIMEOUT;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_BROADCAST_TIMEOUT_SECS;
-import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.resolveSystemApp;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.startAppFast;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.startTestActivity;
@@ -56,6 +54,7 @@
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule;
 import com.android.launcher3.util.rule.FailureWatcher;
@@ -142,7 +141,7 @@
         };
 
         final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
-                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
+                RecentsActivity.ACTIVITY_TRACKER::getCreatedContext);
         mOrderSensitiveRules = RuleChain
                 .outerRule(new SamplerRule())
                 .around(new TestStabilityRule())
@@ -208,13 +207,13 @@
         if (!TestHelpers.isInLauncherProcess()) return null;
         Object[] result = new Object[1];
         Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> {
-            RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+            RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedContext();
             if (activity == null) {
                 return false;
             }
             result[0] = f.apply(activity);
             return true;
-        }).get(), DEFAULT_UI_TIMEOUT, mLauncher);
+        }).get(), mLauncher);
         return (T) result[0];
     }
 
@@ -231,7 +230,7 @@
     private void waitForRecentsActivityStop() {
         try {
             final boolean recentsActivityIsNull = MAIN_EXECUTOR.submit(
-                    () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity() == null).get();
+                    () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedContext() == null).get();
             if (recentsActivityIsNull) {
                 // Null activity counts as a "stopped" one.
                 return;
@@ -244,7 +243,7 @@
 
         Wait.atMost("Recents activity didn't stop",
                 () -> getFromRecents(recents -> !recents.isStarted()),
-                DEFAULT_UI_TIMEOUT, mLauncher);
+                mLauncher);
     }
 
     @Test
@@ -254,7 +253,8 @@
         startTestActivity(2);
         waitForRecentsActivityStop();
         Wait.atMost("Expected three apps in the task list",
-                () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+                () -> mLauncher.getRecentTasks().size() >= 3,
+                mLauncher);
 
         checkTestLauncher();
         BaseOverview overview = mLauncher.getLaunchedAppState().switchToOverview();
@@ -282,7 +282,7 @@
         assertNotNull("OverviewTask.open returned null", task.open());
         assertTrue("Test activity didn't open from Overview", TestHelpers.wait(Until.hasObject(
                 By.pkg(getAppPackageName()).text("TestActivity2")),
-                DEFAULT_UI_TIMEOUT));
+                TestUtil.DEFAULT_UI_TIMEOUT));
 
 
         // Test dismissing a task.
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 4459ed6..77f4c05 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -57,8 +57,6 @@
 
     static final String TAG = "QuickStepOnOffRule";
 
-    public static final int WAIT_TIME_MS = 10000;
-
     public enum Mode {
         THREE_BUTTON, ZERO_BUTTON, ALL
     }
@@ -179,12 +177,13 @@
         }
 
         Wait.atMost("Couldn't switch to " + overlayPackage,
-                () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher);
+                () -> launcher.getNavigationModel() == expectedMode,
+                launcher);
 
         Wait.atMost(() -> "Switching nav mode: "
                         + launcher.getNavigationModeMismatchError(false),
                 () -> launcher.getNavigationModeMismatchError(false) == null,
-                WAIT_TIME_MS, launcher);
+                launcher);
         AbstractLauncherUiTest.checkDetectedLeaks(launcher, false);
         return true;
     }
diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index 298dd6c..f5d082d 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -288,6 +289,34 @@
         assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
     }
 
+    @Test
+    public void testSimpleOrientationTouchTransformer() {
+        final DisplayController displayController = mock(DisplayController.class);
+        doReturn(mInfo).when(displayController).getInfo();
+        final SimpleOrientationTouchTransformer transformer =
+                new SimpleOrientationTouchTransformer(getApplicationContext(), displayController);
+        final MotionEvent move1 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.transform(move1, Surface.ROTATION_90);
+        // The position is transformed to 90 degree.
+        assertEquals(10, move1.getX(), 0f /* delta */);
+        assertEquals(NORMAL_SCREEN_SIZE.getWidth() - 100, move1.getY(), 0f /* delta */);
+
+        // If the touching state is specified, the position is still transformed to 90 degree even
+        // if the given rotation is changed.
+        final MotionEvent move2 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.updateTouchingOrientation(Surface.ROTATION_90);
+        transformer.transform(move2, Surface.ROTATION_0);
+        assertEquals(move1.getX(), move2.getX(), 0f /* delta */);
+        assertEquals(move1.getY(), move2.getY(), 0f /* delta */);
+
+        // If the touching state is cleared, it restores to use the given rotation.
+        final MotionEvent move3 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.clearTouchingOrientation();
+        transformer.transform(move3, Surface.ROTATION_0);
+        assertEquals(100, move3.getX(), 0f /* delta */);
+        assertEquals(10, move3.getY(), 0f /* delta */);
+    }
+
     private DisplayController.Info createDisplayInfo(Size screenSize, int rotation) {
         Point displaySize = new Point(screenSize.getWidth(), screenSize.getHeight());
         RotationUtils.rotateSize(displaySize, rotation);
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index 5d00255..b3c486c 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.TestCase.assertNull;
 
 import static org.junit.Assert.assertEquals;
@@ -26,15 +28,20 @@
 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;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
 import com.android.launcher3.util.LooperExecutor;
 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.util.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedTaskInfo;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,7 +59,11 @@
 public class RecentTasksListTest {
 
     @Mock
-    private SystemUiProxy mockSystemUiProxy;
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private SystemUiProxy mSystemUiProxy;
     @Mock
     private TopTaskTracker mTopTaskTracker;
 
@@ -64,22 +75,27 @@
         MockitoAnnotations.initMocks(this);
         LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class);
         KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
-        mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager,
-                mockSystemUiProxy, mTopTaskTracker);
+
+        // Set desktop mode supported
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true);
+
+        mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
+                mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
     }
 
     @Test
-    public void onRecentTasksChanged_doesNotFetchTasks() {
+    public void onRecentTasksChanged_doesNotFetchTasks() throws Exception {
         mRecentTasksList.onRecentTasksChanged();
-        verify(mockSystemUiProxy, times(0))
+        verify(mSystemUiProxy, times(0))
                 .getRecentTasks(anyInt(), anyInt());
     }
 
     @Test
-    public void loadTasksInBackground_onlyKeys_noValidTaskDescription() {
-        GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(
-                new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null);
-        when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+    public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception  {
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(
+                new RecentTaskInfo(), new RecentTaskInfo(), null);
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -91,15 +107,26 @@
     }
 
     @Test
-    public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() {
+    public void loadTasksInBackground_GetRecentTasksException() throws Exception  {
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+                .thenThrow(new SystemUiProxy.GetRecentTasksException("task load failed"));
+
+        RecentTasksList.TaskLoadResult taskList = mRecentTasksList.loadTasksInBackground(
+                Integer.MAX_VALUE, -1, false);
+
+        assertThat(taskList.mRequestId).isEqualTo(-1);
+        assertThat(taskList).isEmpty();
+    }
+
+    @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);
-        when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(task1, task2, null);
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -111,14 +138,14 @@
     }
 
     @Test
-    public void loadTasksInBackground_freeformTask_createsDesktopTask() {
-        ActivityManager.RecentTaskInfo[] tasks = {
+    public void loadTasksInBackground_freeformTask_createsDesktopTask() throws Exception  {
+        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(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
@@ -134,16 +161,16 @@
     }
 
     @Test
-    public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask() {
-        ActivityManager.RecentTaskInfo[] tasks = {
+    public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask()
+            throws Exception {
+        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);
-        when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
@@ -152,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 2087016..2c275f4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -15,20 +15,18 @@
  */
 package com.android.quickstep;
 
-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.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
 
-import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask;
+import com.android.launcher3.tapl.Overview;
+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;
-import com.android.launcher3.util.rule.TestStabilityRule;
+import com.android.quickstep.util.SplitScreenTestUtils;
 
 import org.junit.Test;
 
@@ -70,39 +68,17 @@
 
     @Test
     public void testSplitTaskTapBothIconMenus() {
-        createAndLaunchASplitPair();
+        Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher);
 
-        OverviewTaskMenu taskMenu =
-                mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+        OverviewTaskMenu taskMenu = overview.getCurrentTask().tapMenu();
         assertTrue("App info item not appearing in expanded task menu.",
                 taskMenu.hasMenuItem("App info"));
         taskMenu.touchOutsideTaskMenuToDismiss();
 
-        OverviewTaskMenu splitMenu =
-                mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu(
-                        OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT);
+        OverviewTaskMenu splitMenu = overview.getCurrentTask().tapMenu(
+                        OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT);
         assertTrue("App info item not appearing in expanded split task's menu.",
                 splitMenu.hasMenuItem("App info"));
         splitMenu.touchOutsideTaskMenuToDismiss();
     }
-
-    private void createAndLaunchASplitPair() {
-        clearAllRecentTasks();
-
-        startTestActivity(2);
-        startTestActivity(3);
-
-        if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) {
-            mLauncher.goHome().switchToOverview().getOverviewActions()
-                    .clickSplit()
-                    .getTestActivityTask(2)
-                    .open();
-        } else {
-            mLauncher.goHome().switchToOverview().getCurrentTask()
-                    .tapMenu()
-                    .tapSplitMenuItem()
-                    .getCurrentTask()
-                    .open();
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index 23a29f7..b15b78e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -21,6 +21,7 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
 
 import android.util.Log;
 
@@ -55,8 +56,6 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        initialize(this);
-
         createAndStartPrivateProfileUser();
 
         mDevice.pressHome();
@@ -117,7 +116,6 @@
     }
 
     @Test
-    @ScreenRecordRule.ScreenRecord // b/334946529
     public void testUserInstalledAppIsShownAboveDivider() throws IOException {
         // Ensure that the App is not installed in main user otherwise, it may not be found in
         // PS container.
@@ -142,7 +140,6 @@
     }
 
     @Test
-    @ScreenRecordRule.ScreenRecord // b/334946529
     public void testPrivateSpaceAppLongPressUninstallMenu() throws IOException {
         // Ensure that the App is not installed in main user otherwise, it may not be found in
         // PS container.
@@ -166,8 +163,9 @@
     }
 
     @Test
-    @ScreenRecordRule.ScreenRecord // b/334946529
+    @ScreenRecordRule.ScreenRecord // b/355466672
     public void testPrivateSpaceLockingBehaviour() throws IOException {
+        assumeFalse(mLauncher.isTablet()); // b/367258373
         // Scroll to the bottom of All Apps
         executeOnLauncher(launcher -> launcher.getAppsView().resetAndScrollToPrivateSpaceHeader());
         HomeAllApps homeAllApps = mLauncher.getAllApps();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index 1886ce6..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,8 +31,14 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest {
 
+    public static final String TAG = "TaplStartLauncherViaGestureTests";
+
     static final int STRESS_REPEAT_COUNT = 10;
 
+    private enum TestCase {
+        TO_HOME, TO_OVERVIEW,
+    }
+
     @Override
     @Before
     public void setUp() throws Exception {
@@ -41,28 +49,60 @@
     }
 
     @Test
-    @NavigationModeSwitch
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
     public void testStressPressHome() {
-        for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
-            // Destroy Launcher activity.
-            closeLauncherActivity();
-
-            // The test action.
-            mLauncher.goHome();
-        }
+        runTest(TestCase.TO_HOME);
     }
 
     @Test
-    @NavigationModeSwitch
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON)
+    public void testStressSwipeHome() {
+        runTest(TestCase.TO_HOME);
+    }
+
+    @Test
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
+    public void testStressPressOverview() {
+        runTest(TestCase.TO_OVERVIEW);
+    }
+
+    @Test
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON)
     public void testStressSwipeToOverview() {
+        runTest(TestCase.TO_OVERVIEW);
+    }
+
+    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();
 
             // The test action.
-            mLauncher.getLaunchedAppState().switchToOverview();
+            switch (testCase) {
+                case TO_OVERVIEW:
+                    mLauncher.getLaunchedAppState().switchToOverview();
+                    break;
+                case TO_HOME:
+                    mLauncher.goHome();
+                    break;
+                default:
+                    throw new IllegalStateException("Cannot run test case: " + testCase);
+            }
+            Log.d(TAG, "Loop " + (i + 1) + " runtime="
+                    + (System.currentTimeMillis() - loopStartTime) + "ms");
         }
-        closeLauncherActivity();
-        mLauncher.goHome();
+        Log.d(TAG, "Test runtime=" + (System.currentTimeMillis() - testStartTime) + "ms");
+        switch (testCase) {
+            case TO_OVERVIEW:
+                closeLauncherActivity();
+                mLauncher.goHome();
+                break;
+            case TO_HOME:
+            default:
+                // No-Op
+                break;
+        }
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
new file mode 100644
index 0000000..f58c84e
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.IgnoreLimit
+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
+
+/** Test Desktop windowing in Overview. */
+@AllowedDevices(allowed = [DeviceProduct.CF_TABLET, DeviceProduct.TANGORPRO])
+@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
+class TaplTestsOverviewDesktop : AbstractLauncherUiTest<QuickstepLauncher?>() {
+    @Before
+    fun setup() {
+        val overview = mLauncher.goHome().switchToOverview()
+        if (overview.hasTasks()) {
+            overview.dismissAllTasks()
+        }
+        startTestAppsWithCheck()
+        mLauncher.goHome()
+    }
+
+    @Test
+    @PortraitLandscape
+    fun enterDesktopViaOverviewMenu() {
+        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() }
+            .also { moveTaskToDesktop(TEST_ACTIVITY_1) }
+
+        TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+
+        // Launch static DesktopTaskView
+        val desktop =
+            mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+        TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+
+        // Launch live-tile DesktopTaskView
+        desktop.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+        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)
+            executeOnLauncher { launcher ->
+                assertWithMessage(
+                        "Launcher activity is the top activity; expecting TestActivity$it"
+                    )
+                    .that(isInLaunchedApp(launcher))
+                    .isTrue()
+            }
+        }
+    }
+
+    private fun assertTestAppLaunched(index: Int) {
+        assertWithMessage("TestActivity$index not opened in Desktop")
+            .that(
+                mDevice.wait(
+                    Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")),
+                    TestUtil.DEFAULT_UI_TIMEOUT,
+                )
+            )
+            .isTrue()
+    }
+
+    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/TaplTestsPersistentTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
index df73e09..a16811e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
@@ -15,16 +15,12 @@
  */
 package com.android.quickstep;
 
-import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
-
 import android.graphics.Rect;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
-import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
 import org.junit.Assert;
 import org.junit.Test;
@@ -35,8 +31,6 @@
 public class TaplTestsPersistentTaskbar extends AbstractTaplTestsTaskbar {
 
     @Test
-    @TaskbarModeSwitch(mode = PERSISTENT)
-    @PortraitLandscape
     @NavigationModeSwitch
     public void testTaskbarFillsWidth() {
         // Width check is performed inside TAPL whenever getTaskbar() is called.
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 1dfab26..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",
@@ -266,9 +267,6 @@
         return launcher.<RecentsView>getOverviewPanel().getBottomRowTaskCountForTablet();
     }
 
-    // Staging; will be promoted to presubmit if stable
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
-
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
@@ -293,9 +291,6 @@
         }
     }
 
-    // Staging; will be promoted to presubmit if stable
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
-
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
@@ -401,7 +396,6 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
     public void testQuickSwitchFromHome() throws Exception {
         startTestActivity(2);
         mLauncher.goHome().quickSwitchToPreviousApp();
@@ -455,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();
@@ -483,7 +477,8 @@
 //        assertTrue("Launcher internal state didn't remain in Overview",
 //                isInState(() -> LauncherState.OVERVIEW));
 //        overview.getCurrentTask().dismiss();
-//        executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple dismissals",
+//        executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple
+//        dismissals",
 //                (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet(
 //                        launcher)) <= 1)));
 
@@ -569,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);
@@ -582,7 +577,7 @@
     public void testExcludeFromRecents() throws Exception {
         startExcludeFromRecentsTestActivity();
         OverviewTask currentTask = getAndAssertLaunchedApp().switchToOverview().getCurrentTask();
-        // TODO(b/326565120): the expected content description shouldn't be null but for now there
+        // TODO(b/342627272): the expected content description shouldn't be null but for now there
         // is a bug that causes it to sometimes be for excludeForRecents tasks.
         assertTrue("Can't find ExcludeFromRecentsTestActivity after entering Overview from it",
                 currentTask.containsContentDescription("ExcludeFromRecents")
@@ -592,7 +587,8 @@
         if (overview.hasTasks()) {
             currentTask = overview.getCurrentTask();
             assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home",
-                    currentTask.containsContentDescription("ExcludeFromRecents")
+                    currentTask.containsContentDescription(
+                            "ExcludeFromRecents")
                             || currentTask.containsContentDescription(null));
         } else {
             // Presumably the test started with 0 tasks and remains that way after going home.
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index 733ea4e..daa4ec3 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -17,8 +17,6 @@
 
 
 import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -33,7 +31,7 @@
 import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.Taskbar;
 import com.android.launcher3.tapl.TaskbarAppIcon;
-import com.android.launcher3.util.rule.TestStabilityRule;
+import com.android.quickstep.util.SplitScreenTestUtils;
 import com.android.wm.shell.Flags;
 
 import org.junit.After;
@@ -110,9 +108,8 @@
         assumeTrue("App pairs feature is currently not enabled, no test needed",
                 Flags.enableAppPairs());
 
-        createAndLaunchASplitPair();
+        Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher);
 
-        Overview overview = mLauncher.goHome().switchToOverview();
         if (mLauncher.isGridOnlyOverviewEnabled() || !mLauncher.isTablet()) {
             assertTrue("Save app pair menu item is missing",
                     overview.getCurrentTask()
@@ -156,24 +153,4 @@
         TaskbarAppIcon firstApp = taskbar.getAppIcon(firstAppName);
         firstApp.launchIntoSplitScreen();
     }
-
-    private void createAndLaunchASplitPair() {
-        clearAllRecentTasks();
-
-        startTestActivity(2);
-        startTestActivity(3);
-
-        if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) {
-            mLauncher.goHome().switchToOverview().getOverviewActions()
-                    .clickSplit()
-                    .getTestActivityTask(2)
-                    .open();
-        } else {
-            mLauncher.goHome().switchToOverview().getCurrentTask()
-                    .tapMenu()
-                    .tapSplitMenuItem()
-                    .getCurrentTask()
-                    .open();
-        }
-    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index ec245ee..c24e974 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -24,6 +24,7 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -133,6 +134,7 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/349439239
     public void testLaunchAppInSplitscreen_fromTaskbarAllApps() {
         getTaskbar().openAllApps()
                 .getAppIcon(TEST_APP_NAME)
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index 2c23f86..710ad6f 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -16,8 +16,6 @@
 
 package com.android.quickstep;
 
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
 
 import static org.junit.Assert.assertNotNull;
@@ -32,8 +30,6 @@
 import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
-import com.android.launcher3.util.rule.ScreenRecordRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.After;
@@ -95,8 +91,6 @@
     @Test
     @PortraitLandscape
     @NavigationModeSwitch
-    @ScreenRecordRule.ScreenRecord // b/336606166
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/336606166
     public void switchToOverview() throws Exception {
         assumeTrue(mLauncher.isTablet());
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
index 2d79623..633a575 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -16,9 +16,9 @@
 
 package com.android.quickstep;
 
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
+import static org.junit.Assert.assertEquals;
 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;
@@ -59,8 +64,6 @@
 
     @Test
     public void startRecentsActivity_allowBackgroundLaunch() {
-        assumeTrue(TaskAnimationManager.ENABLE_SHELL_TRANSITIONS);
-
         final LauncherActivityInterface activityInterface = mock(LauncherActivityInterface.class);
         final GestureState gestureState = mock(GestureState.class);
         final RecentsAnimationCallbacks.RecentsAnimationListener listener =
@@ -70,8 +73,9 @@
 
         final ArgumentCaptor<ActivityOptions> optionsCaptor =
                 ArgumentCaptor.forClass(ActivityOptions.class);
-        verify(mSystemUiProxy).startRecentsActivity(any(), optionsCaptor.capture(), any());
-        assertTrue(optionsCaptor.getValue()
-                .isPendingIntentBackgroundActivityLaunchAllowedByPermission());
+        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/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
index 512557b..dc1da69 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
@@ -88,18 +88,6 @@
     }
 
     @Test
-    public void showBorderOnHoverEvent() {
-        mTaskView.setBorderEnabled(/* enabled= */ true);
-        MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
-        mTaskView.onHoverEvent(MotionEvent.obtain(event));
-        verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
-                true);
-        mTaskView.onFocusChanged(true, 0, new Rect());
-        verify(mFocusAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
-                true);
-    }
-
-    @Test
     public void showBorderOnBorderEnabled() {
         presetBorderStatus(/* enabled= */ false);
         mTaskView.setBorderEnabled(/* enabled= */ true);
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/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
index 4d10f0f..cb59f7d 100644
--- a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
@@ -55,7 +55,6 @@
     private val taskbarDragLayer = mock<TaskbarDragLayer>()
     private val taskbarSharedState = mock<TaskbarSharedState>()
     private var isInDesktopMode = false
-    private val isInDesktopModeProvider = { isInDesktopMode }
     private val launcherPrefs =
         mock<LauncherPrefs> {
             on { get(TASKBAR_PINNING) } doReturn false
@@ -71,8 +70,9 @@
         whenever(taskbarActivityContext.launcherPrefs).thenReturn(launcherPrefs)
         whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer)
         whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager)
-        pinningController =
-            spy(TaskbarPinningController(taskbarActivityContext, isInDesktopModeProvider))
+        whenever(taskbarControllers.taskbarDesktopModeController.areDesktopTasksVisible)
+            .thenAnswer { _ -> isInDesktopMode }
+        pinningController = spy(TaskbarPinningController(taskbarActivityContext))
         pinningController.init(taskbarControllers, taskbarSharedState)
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
new file mode 100644
index 0000000..99c74be
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
@@ -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 androidx.test.uiautomator.By
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.launcher3.tapl.Overview
+import com.android.launcher3.tapl.OverviewTask
+import com.android.launcher3.ui.AbstractLauncherUiTest
+
+object SplitScreenTestUtils {
+
+    /** Creates 2 tasks and makes a split mode pair. Also asserts the accessibility labels. */
+    @JvmStatic
+    fun createAndLaunchASplitPairInOverview(launcher: LauncherInstrumentation): Overview {
+        clearAllRecentTasks(launcher)
+
+        AbstractLauncherUiTest.startTestActivity(2)
+        AbstractLauncherUiTest.startTestActivity(3)
+
+        val overView = launcher.goHome().switchToOverview()
+        if (launcher.isTablet && !launcher.isGridOnlyOverviewEnabled) {
+            overView.overviewActions.clickSplit().getTestActivityTask(2).open()
+        } else {
+            overView.currentTask.tapMenu().tapSplitMenuItem().currentTask.open()
+        }
+
+        val overviewWithSplitPair = launcher.goHome().switchToOverview()
+        val currentTask = overviewWithSplitPair.currentTask
+        currentTask.containsContentDescription(
+            By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(),
+            OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT,
+        )
+        currentTask.containsContentDescription(
+            By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(),
+            OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT,
+        )
+        return overviewWithSplitPair
+    }
+
+    private fun clearAllRecentTasks(launcher: LauncherInstrumentation) {
+        if (launcher.recentTasks.isNotEmpty()) {
+            launcher.goHome().switchToOverview().dismissAllTasks()
+        }
+    }
+}
diff --git a/res/color-night-v31/material_color_surface_bright.xml b/res/color-night-v31/material_color_surface_bright.xml
deleted file mode 100644
index f34ed6c..0000000
--- a/res/color-night-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="24" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container.xml b/res/color-night-v31/material_color_surface_container.xml
deleted file mode 100644
index 002b88e..0000000
--- a/res/color-night-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="12" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_high.xml b/res/color-night-v31/material_color_surface_container_high.xml
deleted file mode 100644
index edd36fc..0000000
--- a/res/color-night-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="17" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_highest.xml b/res/color-night-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e54f953..0000000
--- a/res/color-night-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="22" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_low.xml b/res/color-night-v31/material_color_surface_container_low.xml
deleted file mode 100644
index 40f0d4c..0000000
--- a/res/color-night-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="10" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_lowest.xml b/res/color-night-v31/material_color_surface_container_lowest.xml
index 24f559b..4396f6d 100644
--- a/res/color-night-v31/material_color_surface_container_lowest.xml
+++ b/res/color-night-v31/material_color_surface_container_lowest.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+<?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.
@@ -14,6 +13,7 @@
   ~ 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="@android:color/system_neutral1_500" android:lStar="4" />
 </selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_dim.xml b/res/color-night-v31/material_color_surface_dim.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_inverse.xml b/res/color-night-v31/material_color_surface_inverse.xml
deleted file mode 100644
index ac63072..0000000
--- a/res/color-night-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_variant.xml b/res/color-night-v31/material_color_surface_variant.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/popup_color_background.xml b/res/color-night-v31/popup_color_background.xml
deleted file mode 100644
index 13ceaa0..0000000
--- a/res/color-night-v31/popup_color_background.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
--->
-<selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item
-        android:color="@android:color/system_neutral1_900"
-        android:lStar="12" />
-</selector>
diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml
index 83822a6..28995e3 100644
--- a/res/color-night-v31/popup_shade_first.xml
+++ b/res/color-night-v31/popup_shade_first.xml
@@ -12,7 +12,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorSurfaceContainer"/>
 </selector>
diff --git a/res/color-v31/material_color_surface.xml b/res/color-v31/material_color_surface.xml
deleted file mode 100644
index b049851..0000000
--- a/res/color-v31/material_color_surface.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?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.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_bright.xml b/res/color-v31/material_color_surface_bright.xml
deleted file mode 100644
index b049851..0000000
--- a/res/color-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?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.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container.xml b/res/color-v31/material_color_surface_container.xml
deleted file mode 100644
index b031c08..0000000
--- a/res/color-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?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.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="94" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_high.xml b/res/color-v31/material_color_surface_container_high.xml
deleted file mode 100644
index a996d51..0000000
--- a/res/color-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?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.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="92" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_highest.xml b/res/color-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e7a535a..0000000
--- a/res/color-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?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.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="90" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_low.xml b/res/color-v31/material_color_surface_container_low.xml
deleted file mode 100644
index b8fe01e..0000000
--- a/res/color-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?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.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="96" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_lowest.xml b/res/color-v31/material_color_surface_container_lowest.xml
index 25e8666..f726aea 100644
--- a/res/color-v31/material_color_surface_container_lowest.xml
+++ b/res/color-v31/material_color_surface_container_lowest.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+<?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.
@@ -14,6 +13,7 @@
   ~ 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="@android:color/system_neutral1_500" android:lStar="100" />
 </selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_dim.xml b/res/color-v31/material_color_surface_dim.xml
deleted file mode 100644
index e2d226f..0000000
--- a/res/color-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="87" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_inverse.xml b/res/color-v31/material_color_surface_inverse.xml
deleted file mode 100644
index e189862..0000000
--- a/res/color-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
-  -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_variant.xml b/res/color-v31/material_color_surface_variant.xml
deleted file mode 100644
index e2d226f..0000000
--- a/res/color-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="87" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/popup_color_background.xml b/res/color-v31/popup_color_background.xml
deleted file mode 100644
index 99155d8..0000000
--- a/res/color-v31/popup_color_background.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
--->
-<selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item
-        android:color="@android:color/system_neutral1_50"
-        android:lStar="94" />
-</selector>
diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml
index 1278bb4..be73698 100644
--- a/res/color-v31/popup_shade_first.xml
+++ b/res/color-v31/popup_shade_first.xml
@@ -13,7 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorSurfaceContainer"/>
 </selector>
diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml
index 1dd8da6..0b317bd 100644
--- a/res/color/overview_button.xml
+++ b/res/color/overview_button.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item
         android:alpha="1"
-        android:color="?androidprv:attr/materialColorOnSurface"
+        android:color="?attr/materialColorOnSurface"
         android:state_enabled="true" />
     <item
         android:alpha="?android:disabledAlpha"
-        android:color="?androidprv:attr/materialColorOnSurface"
+        android:color="?attr/materialColorOnSurface"
         android:state_enabled="false" />
 </selector>
\ No newline at end of file
diff --git a/res/color/popup_color_background.xml b/res/color/popup_color_background.xml
deleted file mode 100644
index e87e772..0000000
--- a/res/color/popup_color_background.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?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.
--->
-<selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:color="?attr/popupColorBackground" />
-</selector>
diff --git a/res/color/popup_shade_first.xml b/res/color/popup_shade_first.xml
index 1278bb4..be73698 100644
--- a/res/color/popup_shade_first.xml
+++ b/res/color/popup_shade_first.xml
@@ -13,7 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorSurfaceContainer"/>
 </selector>
diff --git a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml b/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml
deleted file mode 100644
index 47f2a5d..0000000
--- a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?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="52dp"
-    android:height="52dp"
-    android:viewportWidth="52"
-    android:viewportHeight="52">
-  <path
-      android:pathData="M15.5,19C14.538,19 13.715,18.65 13.033,17.968C12.35,17.285 12,16.462 12,15.5C12,14.538 12.35,13.715 13.033,13.033C13.715,12.35 14.538,12 15.5,12C16.462,12 17.285,12.35 17.968,13.033C18.65,13.715 19,14.538 19,15.5C19,16.462 18.65,17.285 17.968,17.968C17.285,18.65 16.462,19 15.5,19Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M26,19C25.038,19 24.215,18.65 23.532,17.968C22.85,17.285 22.5,16.462 22.5,15.5C22.5,14.538 22.85,13.715 23.532,13.033C24.215,12.35 25.038,12 26,12C26.962,12 27.785,12.35 28.468,13.033C29.15,13.715 29.5,14.538 29.5,15.5C29.5,16.462 29.15,17.285 28.468,17.968C27.785,18.65 26.962,19 26,19Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M36.5,19C35.537,19 34.715,18.65 34.033,17.968C33.35,17.285 33,16.462 33,15.5C33,14.538 33.35,13.715 34.033,13.033C34.715,12.35 35.537,12 36.5,12C37.463,12 38.285,12.35 38.967,13.033C39.65,13.715 40,14.538 40,15.5C40,16.462 39.65,17.285 38.967,17.968C38.285,18.65 37.463,19 36.5,19Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M15.5,29.5C14.538,29.5 13.715,29.15 13.033,28.468C12.35,27.785 12,26.962 12,26C12,25.038 12.35,24.215 13.033,23.532C13.715,22.85 14.538,22.5 15.5,22.5C16.462,22.5 17.285,22.85 17.968,23.532C18.65,24.215 19,25.038 19,26C19,26.962 18.65,27.785 17.968,28.468C17.285,29.15 16.462,29.5 15.5,29.5Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M26,29.5C25.038,29.5 24.215,29.15 23.532,28.468C22.85,27.785 22.5,26.962 22.5,26C22.5,25.038 22.85,24.215 23.532,23.532C24.215,22.85 25.038,22.5 26,22.5C26.962,22.5 27.785,22.85 28.468,23.532C29.15,24.215 29.5,25.038 29.5,26C29.5,26.962 29.15,27.785 28.468,28.468C27.785,29.15 26.962,29.5 26,29.5Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M36.5,29.5C35.537,29.5 34.715,29.15 34.033,28.468C33.35,27.785 33,26.962 33,26C33,25.038 33.35,24.215 34.033,23.532C34.715,22.85 35.537,22.5 36.5,22.5C37.463,22.5 38.285,22.85 38.967,23.532C39.65,24.215 40,25.038 40,26C40,26.962 39.65,27.785 38.967,28.468C38.285,29.15 37.463,29.5 36.5,29.5Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M15.5,40C14.538,40 13.715,39.65 13.033,38.967C12.35,38.285 12,37.463 12,36.5C12,35.537 12.35,34.715 13.033,34.033C13.715,33.35 14.538,33 15.5,33C16.462,33 17.285,33.35 17.968,34.033C18.65,34.715 19,35.537 19,36.5C19,37.463 18.65,38.285 17.968,38.967C17.285,39.65 16.462,40 15.5,40Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M26,40C25.038,40 24.215,39.65 23.532,38.967C22.85,38.285 22.5,37.463 22.5,36.5C22.5,35.537 22.85,34.715 23.532,34.033C24.215,33.35 25.038,33 26,33C26.962,33 27.785,33.35 28.468,34.033C29.15,34.715 29.5,35.537 29.5,36.5C29.5,37.463 29.15,38.285 28.468,38.967C27.785,39.65 26.962,40 26,40Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M36.5,40C35.537,40 34.715,39.65 34.033,38.967C33.35,38.285 33,37.463 33,36.5C33,35.537 33.35,34.715 34.033,34.033C34.715,33.35 35.537,33 36.5,33C37.463,33 38.285,33.35 38.967,34.033C39.65,34.715 40,35.537 40,36.5C40,37.463 39.65,38.285 38.967,38.967C38.285,39.65 37.463,40 36.5,40Z"
-      android:fillColor="#40484B"/>
-</vector>
diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml
index e279fa0..39af989 100644
--- a/res/drawable/add_item_dialog_background.xml
+++ b/res/drawable/add_item_dialog_background.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle" >
-    <solid android:color="@color/material_color_surface_container_highest" />
+    <solid android:color="?attr/widgetPickerPrimarySurfaceColor" />
     <corners
         android:topLeftRadius="?android:attr/dialogCornerRadius"
         android:topRightRadius="?android:attr/dialogCornerRadius" />
diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml
index 1e7cff2..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="@color/material_color_surface_bright" />
-                </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="@color/material_color_primary" />
-                </shape>
-            </item>
-        </selector>
+        </ripple>
     </item>
-
-</ripple>
\ No newline at end of file
+    <item android:id="@+id/selected" android:state_focused="false" android:state_selected="true">
+        <ripple android:color="@color/accent_ripple_color">
+            <item>
+                <selector android:enterFadeDuration="100">
+                    <item android:drawable="@drawable/all_apps_tabs_background_selected" />
+                </selector>
+            </item>
+        </ripple>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_selected.xml b/res/drawable/all_apps_tabs_background_selected.xml
new file mode 100644
index 0000000..47f95dd
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_selected.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+        <item
+        android:bottom="@dimen/all_apps_tabs_focus_width"
+        android:end="@dimen/all_apps_tabs_focus_width"
+        android:start="@dimen/all_apps_tabs_focus_width"
+        android:top="@dimen/all_apps_tabs_focus_width">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
+            <solid android:color="?attr/materialColorPrimary" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_selected_focused.xml b/res/drawable/all_apps_tabs_background_selected_focused.xml
new file mode 100644
index 0000000..e3d86c0
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_selected_focused.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <corners android:radius="16dp" />
+            <solid android:color="?attr/materialColorPrimary" />
+        </shape>
+    </item>
+
+    <item
+        android:bottom="@dimen/all_apps_tabs_focus_border"
+        android:end="@dimen/all_apps_tabs_focus_border"
+        android:start="@dimen/all_apps_tabs_focus_border"
+        android:top="@dimen/all_apps_tabs_focus_border">
+        <shape android:shape="rectangle">
+            <corners android:radius="13dp" />
+            <solid android:color="?attr/materialColorPrimary" />
+            <stroke
+                android:width="@dimen/all_apps_tabs_focus_padding"
+                android:color="?attr/materialColorSurfaceDim" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_unselected.xml b/res/drawable/all_apps_tabs_background_unselected.xml
new file mode 100644
index 0000000..ab592a8
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_unselected.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+     <item
+        android:bottom="@dimen/all_apps_tabs_focus_width"
+        android:end="@dimen/all_apps_tabs_focus_width"
+        android:start="@dimen/all_apps_tabs_focus_width"
+        android:top="@dimen/all_apps_tabs_focus_width">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
+            <solid android:color="?attr/materialColorSurfaceBright" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_unselected_focused.xml b/res/drawable/all_apps_tabs_background_unselected_focused.xml
new file mode 100644
index 0000000..0016102
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_unselected_focused.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <corners android:radius="16dp" />
+            <solid android:color="?attr/materialColorPrimary" />
+        </shape>
+    </item>
+
+    <item
+        android:bottom="@dimen/all_apps_tabs_focus_border"
+        android:end="@dimen/all_apps_tabs_focus_border"
+        android:start="@dimen/all_apps_tabs_focus_border"
+        android:top="@dimen/all_apps_tabs_focus_border">
+        <shape android:shape="rectangle">
+            <corners android:radius="13dp" />
+            <solid android:color="?attr/materialColorSurfaceBright" />
+            <stroke
+                android:width="@dimen/all_apps_tabs_focus_padding"
+                android:color="?attr/materialColorSurfaceDim" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_letter_list_text.xml b/res/drawable/bg_letter_list_text.xml
new file mode 100644
index 0000000..bfdd35c
--- /dev/null
+++ b/res/drawable/bg_letter_list_text.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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="?attr/materialColorSurface" />
+    <corners android:radius="100dp"/>
+    <size
+        android:width="@dimen/bg_letter_list_text_size"
+        android:height="@dimen/bg_letter_list_text_size"/>
+</shape>
\ No newline at end of file
diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
index 379e0e5..a19465d 100644
--- a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
+++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
@@ -15,8 +15,7 @@
 -->
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle" >
-    <solid android:color="?androidprv:attr/materialColorOutlineVariant"/>
+    <solid android:color="?attr/materialColorOutlineVariant"/>
     <corners android:radius="@dimen/bottom_sheet_handle_corner_radius" />
 </shape>
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/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml
index f5b6886..13959f6 100644
--- a/res/drawable/button_top_rounded_bordered_ripple.xml
+++ b/res/drawable/button_top_rounded_bordered_ripple.xml
@@ -25,7 +25,7 @@
                     android:topRightRadius="12dp"
                     android:bottomLeftRadius="4dp"
                     android:bottomRightRadius="4dp"  />
-                <solid android:color="@color/material_color_surface_container_highest"/>
+                <solid android:color="?attr/materialColorSurfaceContainerHighest"/>
                 <stroke
                     android:width="2dp"
                     android:color="@color/button_bg"/>
diff --git a/res/drawable/cloud_download_24px.xml b/res/drawable/cloud_download_24px.xml
new file mode 100644
index 0000000..6f7c95a
--- /dev/null
+++ b/res/drawable/cloud_download_24px.xml
@@ -0,0 +1,11 @@
+<!-- GM3 icon cloud_download:vd_theme_24 -->
+<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/white"
+      android:pathData="M260,800Q169,800 104.5,737Q40,674 40,583Q40,505 87,444Q134,383 210,366Q227,294 295,229Q363,164 440,164Q473,164 496.5,187.5Q520,211 520,244L520,486L584,424L640,480L480,640L320,480L376,424L440,486L440,244Q364,258 322,317.5Q280,377 280,440L260,440Q202,440 161,481Q120,522 120,580Q120,638 161,679Q202,720 260,720L740,720Q782,720 811,691Q840,662 840,620Q840,578 811,549Q782,520 740,520L680,520L680,440Q680,392 658,350.5Q636,309 600,280L600,187Q674,222 717,290.5Q760,359 760,440L760,440L760,440Q829,448 874.5,499.5Q920,551 920,620Q920,695 867.5,747.5Q815,800 740,800L260,800ZM480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442L480,442L480,442Q480,442 480,442Q480,442 480,442Z"/>
+</vector>
diff --git a/res/drawable/cloud_download_semibold_24px.xml b/res/drawable/cloud_download_semibold_24px.xml
new file mode 100644
index 0000000..ef15f9f
--- /dev/null
+++ b/res/drawable/cloud_download_semibold_24px.xml
@@ -0,0 +1,11 @@
+<!-- From GM3 icon cloud_download:wght600_vd_theme_24 -->
+<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/white"
+      android:pathData="M260,819.22Q162.22,819.22 92.35,751.7Q22.48,684.17 22.48,586.63Q22.48,503.03 70.61,437.64Q118.74,372.26 198.13,354.13Q217.39,283.26 284.26,217.98Q351.13,152.7 428.7,152.7Q467.91,152.7 496.22,179.3Q524.52,205.91 524.52,244L524.52,484.3L584,425.7L646.22,487.91L480,654.13L313.78,487.91L376,425.7L435.48,484.3L435.48,252.48Q365.13,269.87 326.8,327.11Q288.48,384.35 288.48,446.78L261.7,446.78Q206.51,446.78 167.49,485.8Q128.48,524.81 128.48,580Q128.48,635.74 167,674.48Q205.51,713.22 260,713.22L740,713.22Q778.04,713.22 804.78,686.48Q831.52,659.74 831.52,620Q831.52,581.39 804.78,554.09Q778.04,526.78 738.3,526.78L675.48,526.78L675.48,441.7Q675.48,396.52 656.3,358.98Q637.13,321.44 604.52,292.44L604.52,171.74Q680.78,206.74 726.33,275.8Q771.87,344.87 776.39,428.13L776.39,428.13L776.39,428.13Q847.09,442.35 892.31,496.96Q937.52,551.57 937.52,623.39Q937.52,704.99 879.91,762.1Q822.3,819.22 740,819.22L260,819.22ZM480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83L480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83L480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83L480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83L480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83L480,431.83L480,431.83Q480,431.83 480,431.83Q480,431.83 480,431.83Z"/>
+</vector>
diff --git a/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml b/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml
new file mode 100644
index 0000000..b96a596
--- /dev/null
+++ b/res/drawable/desktop_mode_ic_taskbar_menu_new_window.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="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+    <path
+        android:pathData="M15 16V14H13V12.5H15V10.5H16.5V12.5H18.5V14H16.5V16H15ZM3.5 17C3.09722 17 2.74306 16.8542 2.4375 16.5625C2.14583 16.2569 2 15.9028 2 15.5V4.5C2 4.08333 2.14583 3.72917 2.4375 3.4375C2.74306 3.14583 3.09722 3 3.5 3H14.5C14.9167 3 15.2708 3.14583 15.5625 3.4375C15.8542 3.72917 16 4.08333 16 4.5V9H14.5V7H3.5V15.5H13.625V17H3.5ZM3.5 5.5H14.5V4.5H3.5V5.5ZM3.5 5.5V4.5V5.5Z"
+        android:fillColor="#1C1C14"/>
+</vector>
diff --git a/res/drawable/ic_bubble_button.xml b/res/drawable/ic_bubble_button.xml
new file mode 100644
index 0000000..1ed212e
--- /dev/null
+++ b/res/drawable/ic_bubble_button.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="20dp"
+        android:height="20dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M23,5v8h-2V5H3v14h10v2v0H3c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h18C22.1,3 23,3.9 23,5zM10,8v2.59L5.71,6.29L4.29,7.71L8.59,12H6v2h6V8H10zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/>
+</vector>
diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml
index f336eea..e4053e3 100644
--- a/res/drawable/ic_close_work_edu.xml
+++ b/res/drawable/ic_close_work_edu.xml
@@ -20,6 +20,6 @@
     android:viewportWidth="960"
     android:viewportHeight="960">
     <path
-        android:fillColor="@color/material_color_on_surface"
+        android:fillColor="?attr/materialColorOnSurface"
         android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
 </vector>
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_desktop_with_bg.xml b/res/drawable/ic_desktop_with_bg.xml
new file mode 100644
index 0000000..f54285c
--- /dev/null
+++ b/res/drawable/ic_desktop_with_bg.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ 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="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M24,2L24,2A22,22 0,0 1,46 24L46,24A22,22 0,0 1,24 46L24,46A22,22 0,0 1,2 24L2,24A22,22 0,0 1,24 2z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="M32,30H16V18H32V30ZM32,32C33.1,32 34,31.1 34,30V18C34,16.9 33.1,16 32,16H16C14.9,16 14,16.9 14,18V30C14,31.1 14.9,32 16,32H32ZM30,20H23V22H28V24H30V20ZM18,23H27V28H18V23Z"
+      android:fillColor="#000000"
+      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_more_vert_dots.xml b/res/drawable/ic_more_vert_dots.xml
new file mode 100644
index 0000000..c4659f8
--- /dev/null
+++ b/res/drawable/ic_more_vert_dots.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:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.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,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+</vector>
\ No newline at end of file
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_private_space_with_background.xml b/res/drawable/ic_private_space_with_background.xml
index cb37c9a..cc73f6d 100644
--- a/res/drawable/ic_private_space_with_background.xml
+++ b/res/drawable/ic_private_space_with_background.xml
@@ -13,14 +13,13 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:viewportWidth="48"
     android:viewportHeight="48"
     android:width="48dp"
     android:height="48dp">
     <path
         android:pathData="M48 24A24 24 0 0 1 0 24A24 24 0 0 1 48 24Z"
-        android:fillColor="?androidprv:attr/materialColorSurfaceContainerLowest" />
+        android:fillColor="?attr/materialColorSurfaceContainerLowest" />
     <path
         android:pathData="M24.0021 10.6641L13.3354 14.6641V22.7841C13.3354 29.5174 17.8821 35.7974 24.0021 37.3307C30.1221 35.7974 34.6688 29.5174 34.6688 22.7841V14.6641L24.0021 10.6641ZM32.0021 22.7841C32.0021 28.1174 28.6021 33.0507 24.0021 34.5574C19.4021 33.0507 16.0021 28.1307 16.0021 22.7841V16.5174L24.0021 13.5174L32.0021 16.5174V22.7841Z"
         android:fillType="evenOdd"
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/ic_taskbar_all_apps_button.xml b/res/drawable/ic_taskbar_all_apps_button.xml
deleted file mode 100644
index 82fbbea..0000000
--- a/res/drawable/ic_taskbar_all_apps_button.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?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="44dp"
-    android:height="44dp"
-    android:viewportWidth="44"
-    android:viewportHeight="44">
-  <path
-      android:pathData="M13,16C12.175,16 11.47,15.7 10.885,15.115C10.3,14.53 10,13.825 10,13C10,12.175 10.3,11.47 10.885,10.885C11.47,10.3 12.175,10 13,10C13.825,10 14.53,10.3 15.115,10.885C15.7,11.47 16,12.175 16,13C16,13.825 15.7,14.53 15.115,15.115C14.53,15.7 13.825,16 13,16Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M22,16C21.175,16 20.47,15.7 19.885,15.115C19.3,14.53 19,13.825 19,13C19,12.175 19.3,11.47 19.885,10.885C20.47,10.3 21.175,10 22,10C22.825,10 23.53,10.3 24.115,10.885C24.7,11.47 25,12.175 25,13C25,13.825 24.7,14.53 24.115,15.115C23.53,15.7 22.825,16 22,16Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M31,16C30.175,16 29.47,15.7 28.885,15.115C28.3,14.53 28,13.825 28,13C28,12.175 28.3,11.47 28.885,10.885C29.47,10.3 30.175,10 31,10C31.825,10 32.53,10.3 33.115,10.885C33.7,11.47 34,12.175 34,13C34,13.825 33.7,14.53 33.115,15.115C32.53,15.7 31.825,16 31,16Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M13,25C12.175,25 11.47,24.7 10.885,24.115C10.3,23.53 10,22.825 10,22C10,21.175 10.3,20.47 10.885,19.885C11.47,19.3 12.175,19 13,19C13.825,19 14.53,19.3 15.115,19.885C15.7,20.47 16,21.175 16,22C16,22.825 15.7,23.53 15.115,24.115C14.53,24.7 13.825,25 13,25Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M22,25C21.175,25 20.47,24.7 19.885,24.115C19.3,23.53 19,22.825 19,22C19,21.175 19.3,20.47 19.885,19.885C20.47,19.3 21.175,19 22,19C22.825,19 23.53,19.3 24.115,19.885C24.7,20.47 25,21.175 25,22C25,22.825 24.7,23.53 24.115,24.115C23.53,24.7 22.825,25 22,25Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M31,25C30.175,25 29.47,24.7 28.885,24.115C28.3,23.53 28,22.825 28,22C28,21.175 28.3,20.47 28.885,19.885C29.47,19.3 30.175,19 31,19C31.825,19 32.53,19.3 33.115,19.885C33.7,20.47 34,21.175 34,22C34,22.825 33.7,23.53 33.115,24.115C32.53,24.7 31.825,25 31,25Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M13,34C12.175,34 11.47,33.7 10.885,33.115C10.3,32.53 10,31.825 10,31C10,30.175 10.3,29.47 10.885,28.885C11.47,28.3 12.175,28 13,28C13.825,28 14.53,28.3 15.115,28.885C15.7,29.47 16,30.175 16,31C16,31.825 15.7,32.53 15.115,33.115C14.53,33.7 13.825,34 13,34Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M22,34C21.175,34 20.47,33.7 19.885,33.115C19.3,32.53 19,31.825 19,31C19,30.175 19.3,29.47 19.885,28.885C20.47,28.3 21.175,28 22,28C22.825,28 23.53,28.3 24.115,28.885C24.7,29.47 25,30.175 25,31C25,31.825 24.7,32.53 24.115,33.115C23.53,33.7 22.825,34 22,34Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M31,34C30.175,34 29.47,33.7 28.885,33.115C28.3,32.53 28,31.825 28,31C28,30.175 28.3,29.47 28.885,28.885C29.47,28.3 30.175,28 31,28C31.825,28 32.53,28.3 33.115,28.885C33.7,29.47 34,30.175 34,31C34,31.825 33.7,32.53 33.115,33.115C32.53,33.7 31.825,34 31,34Z"
-      android:fillColor="#40484B"/>
-</vector>
diff --git a/res/drawable/ic_transient_taskbar_all_apps_button.xml b/res/drawable/ic_transient_taskbar_all_apps_button.xml
deleted file mode 100644
index 6e740ae..0000000
--- a/res/drawable/ic_transient_taskbar_all_apps_button.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?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="48dp"
-    android:height="48dp"
-    android:viewportWidth="48"
-    android:viewportHeight="48">
-  <path
-      android:pathData="M13.5,17C12.538,17 11.715,16.65 11.033,15.967C10.35,15.285 10,14.462 10,13.5C10,12.538 10.35,11.715 11.033,11.033C11.715,10.35 12.538,10 13.5,10C14.462,10 15.285,10.35 15.967,11.033C16.65,11.715 17,12.538 17,13.5C17,14.462 16.65,15.285 15.967,15.967C15.285,16.65 14.462,17 13.5,17Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M24,17C23.038,17 22.215,16.65 21.532,15.967C20.85,15.285 20.5,14.462 20.5,13.5C20.5,12.538 20.85,11.715 21.532,11.033C22.215,10.35 23.038,10 24,10C24.962,10 25.785,10.35 26.468,11.033C27.15,11.715 27.5,12.538 27.5,13.5C27.5,14.462 27.15,15.285 26.468,15.967C25.785,16.65 24.962,17 24,17Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M34.5,17C33.537,17 32.715,16.65 32.033,15.967C31.35,15.285 31,14.462 31,13.5C31,12.538 31.35,11.715 32.033,11.033C32.715,10.35 33.537,10 34.5,10C35.463,10 36.285,10.35 36.967,11.033C37.65,11.715 38,12.538 38,13.5C38,14.462 37.65,15.285 36.967,15.967C36.285,16.65 35.463,17 34.5,17Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M13.5,27.5C12.538,27.5 11.715,27.15 11.033,26.468C10.35,25.785 10,24.962 10,24C10,23.038 10.35,22.215 11.033,21.532C11.715,20.85 12.538,20.5 13.5,20.5C14.462,20.5 15.285,20.85 15.967,21.532C16.65,22.215 17,23.038 17,24C17,24.962 16.65,25.785 15.967,26.468C15.285,27.15 14.462,27.5 13.5,27.5Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M24,27.5C23.038,27.5 22.215,27.15 21.532,26.468C20.85,25.785 20.5,24.962 20.5,24C20.5,23.038 20.85,22.215 21.532,21.532C22.215,20.85 23.038,20.5 24,20.5C24.962,20.5 25.785,20.85 26.468,21.532C27.15,22.215 27.5,23.038 27.5,24C27.5,24.962 27.15,25.785 26.468,26.468C25.785,27.15 24.962,27.5 24,27.5Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M34.5,27.5C33.537,27.5 32.715,27.15 32.033,26.468C31.35,25.785 31,24.962 31,24C31,23.038 31.35,22.215 32.033,21.532C32.715,20.85 33.537,20.5 34.5,20.5C35.463,20.5 36.285,20.85 36.967,21.532C37.65,22.215 38,23.038 38,24C38,24.962 37.65,25.785 36.967,26.468C36.285,27.15 35.463,27.5 34.5,27.5Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M13.5,38C12.538,38 11.715,37.65 11.033,36.967C10.35,36.285 10,35.463 10,34.5C10,33.537 10.35,32.715 11.033,32.033C11.715,31.35 12.538,31 13.5,31C14.462,31 15.285,31.35 15.967,32.033C16.65,32.715 17,33.537 17,34.5C17,35.463 16.65,36.285 15.967,36.967C15.285,37.65 14.462,38 13.5,38Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M24,38C23.038,38 22.215,37.65 21.532,36.967C20.85,36.285 20.5,35.463 20.5,34.5C20.5,33.537 20.85,32.715 21.532,32.033C22.215,31.35 23.038,31 24,31C24.962,31 25.785,31.35 26.468,32.033C27.15,32.715 27.5,33.537 27.5,34.5C27.5,35.463 27.15,36.285 26.468,36.967C25.785,37.65 24.962,38 24,38Z"
-      android:fillColor="#40484B"/>
-  <path
-      android:pathData="M34.5,38C33.537,38 32.715,37.65 32.033,36.967C31.35,36.285 31,35.463 31,34.5C31,33.537 31.35,32.715 32.033,32.033C32.715,31.35 33.537,31 34.5,31C35.463,31 36.285,31.35 36.967,32.033C37.65,32.715 38,33.537 38,34.5C38,35.463 37.65,36.285 36.967,36.967C36.285,37.65 35.463,38 34.5,38Z"
-      android:fillColor="#40484B"/>
-</vector>
diff --git a/res/drawable/icon_menu_arrow_background.xml b/res/drawable/icon_menu_arrow_background.xml
index 2eb1dfc..6345c2b 100644
--- a/res/drawable/icon_menu_arrow_background.xml
+++ b/res/drawable/icon_menu_arrow_background.xml
@@ -15,14 +15,13 @@
      limitations under the License.
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:autoMirrored="true">
     <gradient
         android:type="linear"
         android:angle="0"
         android:startColor="#00000000"
         android:centerX="0.25"
-        android:centerColor="?androidprv:attr/materialColorSurfaceBright"
-        android:endColor="?androidprv:attr/materialColorSurfaceBright" />
+        android:centerColor="?attr/materialColorSurfaceBright"
+        android:endColor="?attr/materialColorSurfaceBright" />
     <corners android:radius="@dimen/dialogCornerRadius" />
 </shape>
\ No newline at end of file
diff --git a/res/drawable/popup_background.xml b/res/drawable/popup_background.xml
index 6eedecb..4ddd228 100644
--- a/res/drawable/popup_background.xml
+++ b/res/drawable/popup_background.xml
@@ -15,6 +15,6 @@
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
-    <solid android:color="?attr/popupColorBackground"/>
+    <solid android:color="?attr/materialColorSurfaceContainer"/>
     <corners android:radius="@dimen/dialogCornerRadius"/>
 </shape>
\ No newline at end of file
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index e283d3f..ebfa996 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -7,7 +7,7 @@
   ~
   ~      http://www.apache.org/licenses/LICENSE-2.0
   ~
-  ~ Unless required by applicable law or agreed to in writing, software
+  ~ Unless required by applicable law or agreed to in writing, soft]ware
   ~ distributed under the License is distributed on an "AS IS" BASIS,
   ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   ~ See the License for the specific language governing permissions and
@@ -16,15 +16,11 @@
 
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="@color/material_color_surface_container_low" />
+    <solid android:color="?attr/materialColorSurfaceContainerLow" />
     <corners android:radius="@dimen/rounded_button_radius" />
     <stroke
         android:width="1dp"
-        android:color="@color/material_color_surface_container_low" />
-    <padding
-        android:left="@dimen/rounded_button_padding"
-        android:right="@dimen/rounded_button_padding" />
+        android:color="?attr/materialColorSurfaceContainerLow" />
 </shape>
 
diff --git a/res/drawable/widget_picker_tabs_background.xml b/res/drawable/widget_picker_tabs_background.xml
index a874dd8..f6607b7 100644
--- a/res/drawable/widget_picker_tabs_background.xml
+++ b/res/drawable/widget_picker_tabs_background.xml
@@ -13,36 +13,39 @@
      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">
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetBottom="@dimen/widget_apps_tabs_vertical_padding"
+    android:insetTop="@dimen/widget_apps_tabs_vertical_padding">
+    <ripple
+        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 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/widgetPickerTabBackgroundUnselected"/>
-                </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/widgetPickerTabBackgroundUnselected" />
+                    </shape>
+                </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/widgetPickerTabBackgroundSelected"/>
-                </shape>
-            </item>
-        </selector>
-    </item>
-
-</ripple>
\ No newline at end of file
+                <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/widgetPickerTabBackgroundSelected" />
+                    </shape>
+                </item>
+            </selector>
+        </item>
+    </ripple>
+</inset>
\ No newline at end of file
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_card.xml b/res/drawable/work_card.xml
index 9bf2b8d..01ec947 100644
--- a/res/drawable/work_card.xml
+++ b/res/drawable/work_card.xml
@@ -17,7 +17,7 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
-    <solid android:color="@color/material_color_surface_container_highest" />
+    <solid android:color="?attr/materialColorSurfaceContainerHighest" />
     <corners android:radius="@dimen/work_edu_card_radius" />
 </shape>
 
diff --git a/res/drawable/work_mode_fab_background.xml b/res/drawable/work_mode_fab_background.xml
index 6be33e8..5bad965 100644
--- a/res/drawable/work_mode_fab_background.xml
+++ b/res/drawable/work_mode_fab_background.xml
@@ -18,7 +18,10 @@
     <item>
         <shape android:shape="rectangle">
             <corners android:radius="@dimen/work_fab_radius" />
-            <solid android:color="@color/work_fab_bg_color" />
+            <solid android:color="?attr/materialColorPrimary" />
+            <padding
+                android:left="@dimen/work_mode_fab_background_horizontal_padding"
+                android:right="@dimen/work_mode_fab_background_horizontal_padding"/>
         </shape>
     </item>
 </ripple>
diff --git a/res/drawable/work_scheduler_background.xml b/res/drawable/work_scheduler_background.xml
new file mode 100644
index 0000000..6bbf029
--- /dev/null
+++ b/res/drawable/work_scheduler_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/accent_ripple_color">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/work_fab_radius" />
+            <solid android:color="?attr/materialColorPrimary" />
+            <padding
+                android:padding="@dimen/work_scheduler_background_padding" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/res/layout/all_apps_fast_scroller.xml b/res/layout/all_apps_fast_scroller.xml
index 0f1d933..7e16ca5 100644
--- a/res/layout/all_apps_fast_scroller.xml
+++ b/res/layout/all_apps_fast_scroller.xml
@@ -36,4 +36,17 @@
         android:layout_marginEnd="@dimen/fastscroll_end_margin"
         launcher:canThumbDetach="true" />
 
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/scroll_letter_layout"
+        android:layout_width="@dimen/fastscroll_width"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
+        android:layout_alignTop="@+id/all_apps_header"
+        android:layout_marginTop="@dimen/all_apps_header_bottom_padding"
+        android:layout_marginEnd="@dimen/fastscroll_list_letter_end_margin"
+        android:clipToPadding="false"
+        android:outlineProvider="none"
+        />
 </merge>
\ No newline at end of file
diff --git a/res/layout/all_apps_personal_work_tabs.xml b/res/layout/all_apps_personal_work_tabs.xml
index e04b207..ecc5a14 100644
--- a/res/layout/all_apps_personal_work_tabs.xml
+++ b/res/layout/all_apps_personal_work_tabs.xml
@@ -21,8 +21,8 @@
     android:layout_width="match_parent"
     android:layout_height="@dimen/all_apps_header_pill_height"
     android:layout_gravity="center_horizontal"
-    android:paddingTop="@dimen/all_apps_tabs_vertical_padding"
-    android:paddingBottom="@dimen/all_apps_tabs_vertical_padding"
+    android:paddingTop="@dimen/all_apps_tabs_vertical_padding_focus"
+    android:paddingBottom="@dimen/all_apps_tabs_vertical_padding_focus"
     android:layout_marginTop="@dimen/all_apps_tabs_margin_top"
     android:orientation="horizontal"
     style="@style/TextHeadline"
diff --git a/res/layout/fast_scroller_letter_list_text_view.xml b/res/layout/fast_scroller_letter_list_text_view.xml
new file mode 100644
index 0000000..493b6fc
--- /dev/null
+++ b/res/layout/fast_scroller_letter_list_text_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.launcher3.allapps.LetterListTextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/fastscroll_list_letter_size"
+    android:layout_height="@dimen/fastscroll_list_letter_size"
+    android:textSize="@dimen/fastscroll_list_letter_text_size"
+    android:importantForAccessibility="no"
+    android:gravity="center"
+    android:clickable="false">
+</com.android.launcher3.allapps.LetterListTextView>
\ No newline at end of file
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index a709fbc..83c8d6c 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -51,7 +51,7 @@
 
         <!-- Keep these behind the workspace so that they are not visible when
          we go into AllApps -->
-        <com.android.launcher3.pageindicators.WorkspacePageIndicator
+        <com.android.launcher3.pageindicators.PageIndicatorDots
             android:id="@+id/page_indicator"
             android:layout_width="match_parent"
             android:layout_height="@dimen/workspace_page_indicator_height"
diff --git a/res/layout/page_indicator_dots.xml b/res/layout/page_indicator_dots.xml
deleted file mode 100644
index d5fe51e..0000000
--- a/res/layout/page_indicator_dots.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<com.android.launcher3.pageindicators.PageIndicatorDots xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/page_indicator"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/workspace_page_indicator_height"
-    android:layout_gravity="bottom | center_horizontal"
-    android:theme="@style/HomeScreenElementTheme" />
\ No newline at end of file
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
index 9c0f129..6bce220 100644
--- a/res/layout/private_space_header.xml
+++ b/res/layout/private_space_header.xml
@@ -43,6 +43,7 @@
             android:layout_height="@dimen/ps_header_image_height"
             android:background="@drawable/ps_settings_background"
             android:src="@drawable/ic_ps_settings"
+            android:visibility="gone"
             android:contentDescription="@string/ps_container_settings" />
         <LinearLayout
             android:id="@+id/ps_lock_unlock_button"
@@ -60,7 +61,7 @@
                 android:layout_marginBottom="@dimen/ps_lock_icon_margin_bottom"
                 android:importantForAccessibility="no"
                 android:src="@drawable/ic_lock"
-                app:tint="@color/material_color_primary_fixed_dim"
+                app:tint="?attr/materialColorPrimaryFixedDim"
                 android:scaleType="center"/>
             <TextView
                 android:id="@+id/lock_text"
@@ -68,10 +69,12 @@
                 android:layout_height="wrap_content"
                 android:layout_marginStart="@dimen/ps_lock_icon_text_margin_start_expanded"
                 android:layout_marginEnd="@dimen/ps_lock_icon_text_margin_end_expanded"
-                android:textColor="@color/material_color_on_primary_fixed"
+                android:textColor="?attr/materialColorOnPrimaryFixed"
                 android:textSize="14sp"
                 android:text="@string/ps_container_lock_title"
+                android:maxLines="1"
                 android:visibility="gone"
+                android:alpha="0"
                 style="@style/TextHeadline"/>
         </LinearLayout>
     </LinearLayout>
@@ -97,6 +100,7 @@
         android:gravity="center_vertical"
         android:layout_marginStart="@dimen/ps_header_layout_margin"
         android:text="@string/ps_container_title"
+        android:maxLines="1"
         android:theme="@style/PrivateSpaceHeaderTextStyle"
         android:importantForAccessibility="no"/>
 
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 8dc785a..622f0d6 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -104,7 +104,6 @@
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
-                android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
                 android:layout_weight="1"
                 android:background="@drawable/widget_picker_tabs_background"
                 android:text="@string/widgets_full_sheet_personal_tab"
@@ -117,7 +116,6 @@
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
-                android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
                 android:layout_weight="1"
                 android:background="@drawable/widget_picker_tabs_background"
                 android:text="@string/widgets_full_sheet_work_tab"
diff --git a/res/layout/widgets_list_expand_button.xml b/res/layout/widgets_list_expand_button.xml
new file mode 100644
index 0000000..ff2d777
--- /dev/null
+++ b/res/layout/widgets_list_expand_button.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/widget_list_expand_button"
+    style="@style/Button.Rounded.Colored"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="@dimen/widgets_list_expand_button_top_margin"
+    android:background="@drawable/widgets_list_expand_button_background"
+    android:drawablePadding="@dimen/widgets_list_expand_button_drawable_padding"
+    android:drawableStart="@drawable/ic_more_horiz_24"
+    android:drawableTint="?attr/widgetPickerExpandButtonTextColor"
+    android:maxLines="1"
+    android:minHeight="48dp"
+    android:paddingEnd="@dimen/widgets_list_expand_button_end_padding"
+    android:paddingStart="@dimen/widgets_list_expand_button_start_padding"
+    android:paddingVertical="@dimen/widgets_list_expand_button_vertical_padding"
+    android:text="@string/widgets_list_expand_button_label"
+    android:contentDescription="@string/widgets_list_expand_button_content_description"
+    android:textColor="?attr/widgetPickerExpandButtonTextColor" />
\ No newline at end of file
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index ce5eed9..5dc1b47 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -23,9 +23,7 @@
     <com.android.launcher3.views.SpringRelativeLayout
         android:id="@+id/container"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:focusable="true"
-        android:importantForAccessibility="no">
+        android:layout_height="match_parent">
 
         <View
             android:id="@+id/collapse_handle"
@@ -72,7 +70,7 @@
                 android:layout_height="match_parent"
                 android:clipChildren="false"
                 android:clipToPadding="false"
-                android:paddingBottom="24dp"
+                android:paddingBottom="8dp"
                 android:layout_gravity="start"
                 android:layout_weight="0.33">
                 <TextView
@@ -86,14 +84,6 @@
                     android:layout_width="@dimen/fastscroll_width"
                     android:layout_height="match_parent"
                     android:layout_marginEnd="@dimen/fastscroll_end_margin" />
-
-                <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                    android:id="@+id/search_widgets_list_view"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:clipToPadding="false"
-                    android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
-                    android:visibility="gone" />
             </FrameLayout>
 
             <FrameLayout
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 887efb8..0528d3b 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -15,64 +15,67 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto">
 
-    <FrameLayout
+    <LinearLayout
         android:id="@+id/widgets_two_pane_sheet_paged_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:gravity="start"
-        android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
         android:layout_gravity="start"
         android:clipChildren="false"
         android:clipToPadding="false"
-        android:layout_alignParentStart="true">
-        <com.android.launcher3.widget.picker.WidgetPagedView
-            android:id="@+id/widgets_view_pager"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:clipToPadding="false"
-            android:descendantFocusability="afterDescendants"
-            launcher:pageIndicator="@+id/tabs" >
-
-            <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                android:id="@+id/primary_widgets_list_view"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipToPadding="false" />
-
-            <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                android:id="@+id/work_widgets_list_view"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipToPadding="false" />
-
-        </com.android.launcher3.widget.picker.WidgetPagedView>
-
+        android:layout_alignParentStart="true"
+        android:orientation="vertical">
         <!-- SearchAndRecommendationsView without the tab layout as well -->
-        <com.android.launcher3.views.StickyHeaderLayout
+        <!-- Note: the horizontal padding matches with the WidgetPagedView -->
+        <LinearLayout
             android:id="@+id/search_and_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:clipToOutline="true"
+            android:elevation="1dp"
+            android:background="?attr/widgetPickerPrimarySurfaceColor"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
             android:orientation="vertical">
 
-            <FrameLayout
+            <LinearLayout
                 android:id="@+id/search_bar_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
-                android:clipToPadding="false"
-                android:elevation="0.1dp"
-                android:paddingBottom="8dp"
+                android:gravity="center_vertical"
+                android:layout_marginBottom="8dp"
                 launcher:layout_sticky="true">
+                <FrameLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:clipToPadding="false"
+                    android:elevation="0.1dp"
+                    android:paddingBottom="8dp">
 
-                <include layout="@layout/widgets_search_bar" />
-            </FrameLayout>
+                    <include layout="@layout/widgets_search_bar" />
+                </FrameLayout>
+
+                <ImageButton
+                    android:id="@+id/widget_picker_widget_options_menu"
+                    android:layout_width="48dp"
+                    android:layout_height="48dp"
+                    android:layout_marginStart="8dp"
+                    android:layout_marginBottom="8dp"
+                    android:layout_gravity="bottom"
+                    android:background="@drawable/full_rounded_transparent_ripple"
+                    android:contentDescription="@string/widget_picker_widget_options_button_description"
+                    android:padding="12dp"
+                    android:src="@drawable/ic_more_vert_dots"
+                    android:visibility="gone"
+                    android:tint="?attr/widgetPickerWidgetOptionsMenuColor" />
+            </LinearLayout>
 
             <FrameLayout
                 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">
@@ -94,7 +97,6 @@
                     android:layout_width="0dp"
                     android:layout_height="match_parent"
                     android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
-                    android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
                     android:layout_weight="1"
                     android:background="@drawable/widget_picker_tabs_background"
                     android:text="@string/widgets_full_sheet_personal_tab"
@@ -107,7 +109,6 @@
                     android:layout_width="0dp"
                     android:layout_height="match_parent"
                     android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
-                    android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
                     android:layout_weight="1"
                     android:background="@drawable/widget_picker_tabs_background"
                     android:text="@string/widgets_full_sheet_work_tab"
@@ -116,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 f3d3b16..45a9ac0 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -15,43 +15,58 @@
 <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">
 
-            <FrameLayout
+            <LinearLayout
                 android:id="@+id/search_bar_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
-                android:clipToPadding="false"
-                android:elevation="0.1dp"
-                android:paddingBottom="16dp"
                 android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
                 launcher:layout_sticky="true">
+                <FrameLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:clipToPadding="false"
+                    android:elevation="0.1dp"
+                    android:paddingBottom="16dp">
 
-                <include layout="@layout/widgets_search_bar" />
-            </FrameLayout>
+                    <include layout="@layout/widgets_search_bar" />
+                </FrameLayout>
+
+                <ImageButton
+                    android:id="@+id/widget_picker_widget_options_menu"
+                    android:layout_width="48dp"
+                    android:layout_height="48dp"
+                    android:layout_marginStart="8dp"
+                    android:layout_marginBottom="16dp"
+                    android:layout_gravity="bottom"
+                    android:background="@drawable/full_rounded_transparent_ripple"
+                    android:contentDescription="@string/widget_picker_widget_options_button_description"
+                    android:padding="12dp"
+                    android:src="@drawable/ic_more_vert_dots"
+                    android:visibility="gone"
+                    android:tint="?attr/widgetPickerWidgetOptionsMenuColor" />
+            </LinearLayout>
 
             <FrameLayout
                 android:layout_width="match_parent"
@@ -62,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_apps_edu.xml b/res/layout/work_apps_edu.xml
index c581ae3..a45d585 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -44,8 +44,7 @@
         <FrameLayout
             android:layout_width="@dimen/rounded_button_width"
             android:layout_height="@dimen/rounded_button_width"
-            android:background="@drawable/rounded_action_button"
-            android:padding="@dimen/rounded_button_padding">
+            android:background="@drawable/rounded_action_button">
             <ImageButton
                 android:id="@+id/action_btn"
                 android:layout_width="@dimen/x_icon_size"
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index b3484c9..46f2d8a 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -12,42 +12,37 @@
      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: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 90e784d..5181bb7 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Legstukke gedeaktiveer in Veiligmodus"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Kortpad is nie beskikbaar nie"</string>
     <string name="home_screen" msgid="5629429142036709174">"Tuis"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Stel <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as verstektuisskermapp in Instellings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Verdeelde skerm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Programinligting vir %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Gebruikinstellings vir %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nuwe venster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Stoor apppaar"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Hierdie apppaar word nie op hierdie toestel gesteun nie"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Apppaar is nie beskikbaar nie"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Raak en hou om \'n legstuk te skuif."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dubbeltik en hou om \'n legstuk te skuif of gebruik gepasmaakte handelinge."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Meer opsies"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Wys alle legstukke"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d breed by %2$d hoog"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>-legstuk"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Neem notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Voeg by"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Voeg <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-legstuk by"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tik om legstukinstellings te verander"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Verander legstukinstellings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Deursoek programme"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installeer"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Moenie voorstel nie"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Vasspeldvoorspelling"</string>
+    <string name="bubble" msgid="3072951361014076670">"Borrel"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installeer kortpaaie"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Laat \'n program toe om kortpaaie by te voeg sonder gebruikerinmenging."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"lees tuis-instellings en -kortpaaie"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Gedeaktiveer deur jou administrateur"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Laat toe dat tuisskerm gedraai word"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Wanneer foon gedraai word"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Kennisgewingkolle"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aan"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Af"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeer tans; <xliff:g id="PROGRESS">%2$s</xliff:g> voltooi"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> laai tans af, <xliff:g id="PROGRESS">%2$s</xliff:g> voltooid"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> wag tans om te installeer"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is geargiveer. Tik om af te laai en terug te stel."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is geargiveer."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"laai af en stel terug"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Programopdatering word vereis"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Die program vir hierdie ikoon is nie opgedateer nie. Jy kan dit handmatig opdateer om hierdie kortpad weer te aktiveer, of die ikoon verwyder."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Dateer op"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Het dit"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Onderbreek werkprogramme"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Hervat"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Misluk: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privaat ruimte"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 1289b8c..a217bb6 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ምግብሮች በደህንነቱ የተጠበቀ ሁኔታ ተሰናክለዋል"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"አቋራጭ አይገኝም"</string>
     <string name="home_screen" msgid="5629429142036709174">"መነሻ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"በቅንብሮች ውስጥ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>ን እንደ ነባሪ የHome መተግበሪያ ያቀናብሩ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"የተከፈለ ማያ ገፅ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"የመተግበሪያ መረጃ ለ%1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"የ%1$s የአጠቃቀም ቅንብሮች"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"አዲስ መስኮት"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"የመተግበሪያ ጥምረትን ያስቀምጡ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ይህ የመተግበሪያ ጥምረት በዚህ መሣሪያ ላይ አይደገፍም"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"የመተግበሪያ ጥምረት አይገኝም"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ምግብርን ለማንቀሳቀስ ይንኩ እና ይያዙ።"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ምግብርን ለማንቀሳቀስ ወይም ብጁ እርምጃዎችን ለመጠቀም ሁለቴ መታ ያድርጉ እና ይያዙ።"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ተጨማሪ አማራጮች"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ሁሉንም ምግብሮች አሳይ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ስፋት በ%2$d ከፍታ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"የ<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ምግብር"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"የማስታወሻ አያያዝ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"አክል"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"ምግብር <xliff:g id="WIDGET_NAME">%1$s</xliff:g>ን አክል"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"የምግብር ቅንብሮችን ለመለወጥ መታ ያድርጉ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"የምግብር ቅንብሮችን ይለውጡ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"መተግበሪያዎችን ፈልግ"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ጫን"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"መተግበሪያውን አይጠቁሙ"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"የፒን ግምት"</string>
+    <string name="bubble" msgid="3072951361014076670">"አረፋ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"አቋራጮችን ይጭናል"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"መተግበሪያው ያለተጠቃሚ ጣልቃ ገብነት አቋራጭ እንዲያክል ያስችለዋል።"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"የመነሻ ቅንብሮች እና አቋራጮችን ያነባል"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"በእርስዎ አስተዳዳሪ የተሰናከለ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"የመነሻ ማያ ገፅ ማሽከርከርን ይፍቀዱ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ስልኩ ሲዞር"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"የማሳወቂያ ነጥቦች"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"አብራ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ጠፍቷል"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> በመጫን ላይ፣ <xliff:g id="PROGRESS">%2$s</xliff:g> ተጠናቅቋል"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> በመውረድ ላይ፣ <xliff:g id="PROGRESS">%2$s</xliff:g> ተጠናቋል"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ለመጫን በመጠበቅ ላይ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> በማህደር ተቀምጧል። ለማወረድ እና ወደነበረበት ለመመለስ መታ ያድርጉ።"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> በማህደር ተቀምጧል።"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"አውርድ እና ወደነበረበት መልስ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"መተግበሪያ ማዘመን አስፈላጊ ነው"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"የዚህ አዶ መተግበሪያ አልተዘመነም። ይህን አቋራጭ ዳግም ለማንቃት በራስዎ ማዘመን ወይም አዶውን ማስወገድ ይችላሉ።"</string>
     <string name="dialog_update" msgid="2178028071796141234">"አዘምን"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ገባኝ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"የሥራ መተግበሪያዎችን ባሉበት አቁም"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ካቆመበት ቀጥል"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"አጣራ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"አልተሳካም፦ <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"የግል ቦታ"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 37d6fba..c08254a 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"الأدوات غير مفعّلة في الوضع الآمن"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"الاختصار غير متاح"</string>
     <string name="home_screen" msgid="5629429142036709174">"الشاشة الرئيسية"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"يمكن ضبط \"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>\" كتطبيق الشاشة الرئيسية التلقائي من خلال \"الإعدادات\""</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"تقسيم الشاشة"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏معلومات تطبيق %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏إعدادات استخدام \"%1$s\""</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"نافذة جديدة"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"حفظ استخدام التطبيقين معًا"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | ‏<xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"لا يمكن استخدام هذين التطبيقَين في الوقت نفسه على هذا الجهاز"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ميزة \"استخدام تطبيقين في الوقت نفسه\" غير متوفّرة"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"انقر مع الاستمرار لنقل أداة."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"انقر مرتين مع تثبيت إصبعك لنقل أداة أو استخدام الإجراءات المخصّصة."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"خيارات إضافية"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"عرض كل التطبيقات المصغّرة"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏العرض %1$d الطول %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"أداة <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"تدوين الملاحظات"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"إضافة"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"إضافة التطبيق المصغّر \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"انقر لتغيير إعدادات الأداة"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغيير إعدادات الأداة"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"بحث في التطبيقات"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"تثبيت"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"عدم اقتراح التطبيق"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"تثبيت التطبيق المتوقّع"</string>
+    <string name="bubble" msgid="3072951361014076670">"فقاعة"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"تثبيت اختصارات"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"للسماح لتطبيق ما بإضافة اختصارات بدون تدخل المستخدم."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"الاطلاع على الإعدادات والاختصارات على الشاشة الرئيسية"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"أوقف المشرف هذه الميزة"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"السماح بتدوير الشاشة الرئيسية"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"عند تدوير الهاتف"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"نقاط الإشعارات"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"الإعداد مفعّل"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"غير مفعّل"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"جارٍ تثبيت <xliff:g id="NAME">%1$s</xliff:g>، مستوى التقدم: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"جارٍ تنزيل <xliff:g id="NAME">%1$s</xliff:g>، اكتمل <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> في انتظار التثبيت"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"تمت أرشفة تطبيق \"<xliff:g id="NAME">%1$s</xliff:g>\". انقر لتنزيله واستعادته."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"تمت أرشفة \"<xliff:g id="NAME">%1$s</xliff:g>\"."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"تنزيل التطبيق واستعادته"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"مطلوب تحديث التطبيق"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"لم يتمّ تحديث التطبيق الخاص بهذا الرمز. يمكنك تحديث التطبيق يدويًا لإعادة تفعيل هذا الاختصار أو إزالة الرمز."</string>
     <string name="dialog_update" msgid="2178028071796141234">"تحديث"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"حسنًا"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"إيقاف تطبيقات العمل مؤقتًا"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"إلغاء الإيقاف المؤقت"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"فلتر"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"تعذَّر <xliff:g id="WHAT">%1$s</xliff:g>."</string>
     <string name="private_space_label" msgid="2359721649407947001">"مساحة خاصة"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index e983ce8..ccfb10e 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ৱিজেটবোৰক সুৰক্ষিত ম\'ডত অক্ষম কৰা হ’ল"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"শ্বৰ্টকাট নাই"</string>
     <string name="home_screen" msgid="5629429142036709174">"গৃহ স্ক্ৰীন"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ছেটিঙত <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>ক ডিফ’ল্ট গৃহপৃষ্ঠা এপ্‌ হিচাপে ছেট কৰক"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"বিভাজিত স্ক্ৰীন"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$sৰ বাবে এপৰ তথ্য"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sৰ বাবে ব্যৱহাৰৰ ছেটিং"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"নতুন ৱিণ্ড’"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"এপৰ পেয়াৰ ছেভ কৰক"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"এই ডিভাইচটোত এই এপ্‌ পেয়াৰ কৰাৰ সুবিধাটো সমৰ্থিত নহয়"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"এপ্‌ পেয়াৰ কৰাৰ সুবিধাটো উপলব্ধ নহয়"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ৱিজেট স্থানান্তৰ কৰিবলৈ টিপি ধৰি ৰাখক।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"কোনো ৱিজেট স্থানান্তৰ কৰিবলৈ দুবাৰ টিপি ধৰি ৰাখক অথবা কাষ্টম কাৰ্য ব্যৱহাৰ কৰক।"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"অধিক বিকল্প"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"আটাইবোৰ ৱিজেট দেখুৱাওক"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d বহল x %2$d ওখ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ৱিজেট"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"টোকা গ্ৰহণ কৰা"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"যোগ দিয়ক"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ৱিজেট যোগ দিয়ক"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ৱিজেটৰ ছেটিং সলনি কৰিবলৈ টিপক"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ৱিজেটৰ ছেটিং সলনি কৰক"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"এপ্‌সমূহ সন্ধান কৰক"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ইনষ্টল কৰক"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"পৰামৰ্শ নিদিব"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"পূৰ্বানুমান কৰা এপ্‌টো পিন কৰক"</string>
+    <string name="bubble" msgid="3072951361014076670">"বাবল"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"শ্বৰ্টকাট ইনষ্টল কৰিব পাৰে"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ব্য়ৱহাৰকাৰীৰ হস্তক্ষেপ অবিহনেই কোনো এপক শ্বৰ্টকাটবোৰ যোগ কৰাৰ অনুমতি দিয়ে।"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"গৃহ স্ক্ৰীনত ছেটিং আৰু শ্বৰ্টকাটসমূহ পঢ়া"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"আপোনাৰ প্ৰশাসকে অক্ষম কৰি ৰাখিছে"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"গৃহ স্ক্ৰীন ঘূৰোৱাৰ অনুমতি দিয়ক"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ফ\'নটো যেতিয়া ঘূৰোৱা হয়"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"জাননী বিন্দু"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"অন কৰা আছে"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"অফ আছে"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ইনষ্টল কৰি থকা হৈছে, <xliff:g id="PROGRESS">%2$s</xliff:g> সম্পূৰ্ণ হৈছে"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ডাউনল’ড কৰি থকা হৈছে, <xliff:g id="PROGRESS">%2$s</xliff:g> সম্পূৰ্ণ হ’ল"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ইনষ্টল হোৱালৈ অপেক্ষা কৰি থকা হৈছে"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> আৰ্কাইভ কৰা হৈছে। ডাউনল’ড আৰু পুনঃস্থাপন কৰিবলৈ টিপক।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> আৰ্কাইভ কৰা হৈছে।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ডাউনল’ড আৰু পুনঃস্থাপন কৰক"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"এপ্‌টো আপডে’ট কৰা প্ৰয়োজন"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"এই চিহ্নটোৰ এপ্‌টো আপডে’ট কৰা হোৱা নাই। আপুনি এই শ্বৰ্টকাটটো পুনৰ সক্ষম কৰিবলৈ মেনুৱেলী আপডে’ট কৰিব পাৰে অথবা চিহ্নটো আঁতৰাব পাৰে।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"আপডে’ট কৰক"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"বুজি পালোঁ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"কৰ্মস্থানৰ এপ্‌ পজ কৰক"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"আনপজ কৰক"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ফিল্টাৰ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"বিফল: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"প্ৰাইভেট স্পে\'চ"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index f390c7c..c85f271 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Vidcetlər Güvənli rejimdə deaktiv edilib"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Qısayol əlçatan deyil"</string>
     <string name="home_screen" msgid="5629429142036709174">"Əsas səhifə"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ayarlarda <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> tətbiqini defolt əsas ekran tətbiqi kimi ayarlayın"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ekran bölünməsi"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ilə bağlı tətbiq məlumatı"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s üzrə istifadə ayarları"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yeni Pəncərə"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Tətbiq cütünü saxlayın"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Bu tətbiq cütü bu cihazda dəstəklənmir"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Tətbiq cütü əlçatan deyil"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Vidceti daşımaq üçün toxunub saxlayın."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Vidceti daşımaq üçün iki dəfə toxunub saxlayın və ya fərdi əməliyyatlardan istifadə edin."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Digər seçimlər"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Bütün vidcetləri göstərin"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%2$d hündürlük %1$d enində"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidceti"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Qeydgötürmə"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Əlavə edin"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidcet əlavə edin"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Vidcet ayarlarını dəyişmək üçün toxunun"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Vidcet ayarlarını dəyişin"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Tətbiqləri axtarın"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Quraşdırın"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Tətbiq təklif olunmasın"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Proqnozlaşdırılan tətbiqi bərkidin"</string>
+    <string name="bubble" msgid="3072951361014076670">"Qabarcıq"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"qısayolları quraşdır"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Tətbiqə istifadəçi müdaxiləsi olmadan qısayolları əlavə etməyə icazə verir."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"Əsas səhifə ayarlarını və qısayollarını oxumaq"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Admininiz tərəfindən deaktiv edilib"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Əsas ekran çevrilsin"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefon çevrilən zaman"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bildiriş nöqtələri"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktiv"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Deaktiv"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> quraşdırır, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlanıb"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> endirilir, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlandı"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> yüklənmək üçün gözləyir"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> arxivləndi. Toxunaraq endirin və bərpa edin."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> arxivləndi."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"endirin və bərpa edin"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Tətbiqin güncəllənməsi tələb edilir"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Bu ikona üçün tətbiq güncəllənməyib. Bu qısayolu yenidən aktivləşdirmək üçün manual olaraq güncəlləyə və ya ikonanı silə bilərsiniz."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Güncəlləyin"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Anladım"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"İş tətbiqlərini durdurun"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Davam etdirin"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtr"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Alınmadı: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Məxfi sahə"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index fc71eeb..e8b55b7 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Vidžeti su onemogućeni u Bezbednom režimu"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Prečica nije dostupna"</string>
     <string name="home_screen" msgid="5629429142036709174">"Početni ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Podesite <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kao podrazumevanu početnu aplikaciju u Podešavanjima"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podeljeni ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji za: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Podešavanja potrošnje za %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Sačuvaj par aplikacija"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ovaj par aplikacija nije podržan na ovom uređaju"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacija nije dostupan"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dodirnite i zadržite radi pomeranja vidžeta."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvaput dodirnite i zadržite da biste pomerali vidžet ili koristite prilagođene radnje."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Još opcija"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Prikaži sve vidžete"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d×%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"širina od %1$d i visina od %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidžet"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pravljenje beležaka"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodajte vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dodirnite da biste promenili podešavanja vidžeta"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Promenite podešavanja vidžeta"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pretražite aplikacije"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instaliraj"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne predlaži aplikaciju"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Zakači predviđanje"</string>
+    <string name="bubble" msgid="3072951361014076670">"Oblačić"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instaliranje prečica"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Dozvoljava aplikaciji da dodaje prečice bez intervencije korisnika."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"čitanje podešavanja i prečica na početnom ekranu"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administrator je onemogućio"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dozvoli rotaciju početnog ekrana"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kada se telefon rotira"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Tačke za obaveštenja"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Uključeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Isključeno"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> se instalira, <xliff:g id="PROGRESS">%2$s</xliff:g> gotovo"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> se preuzima, završeno je <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> čeka na instaliranje"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana. Dodirnite da biste je preuzeli i vratili."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"preuzmite i vratite"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Treba da ažurirate aplikaciju"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija za ovu ikonu nije ažurirana. Možete da je ručno ažurirate da biste ponovo omogućili ovu prečicu ili uklonite ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ažuriraj"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Važi"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pauziraj poslovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ponovo aktiviraj"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nije uspelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privatni prostor"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 4589125..6d714bc 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Віджэты адключаны ў Бяспечным рэжыме"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Ярлык недаступны"</string>
     <string name="home_screen" msgid="5629429142036709174">"Галоўны экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Зрабіць <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> стандартнай праграмай для галоўнага экрана, перайшоўшы ў Налады"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Падзелены экран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Інфармацыя пра праграму для: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s: налады выкарыстання"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Новае акно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Захаваць спалучэнне праграм"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Дадзенае спалучэнне праграм не падтрымліваецца на гэтай прыладзе"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Спалучэнне праграм недаступнае"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Націсніце і ўтрымлівайце віджэт для перамяшчэння."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Дакраніцеся двойчы і ўтрымлівайце, каб перамясціць віджэт або выкарыстоўваць спецыяльныя дзеянні."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Дадатковыя параметры"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Паказваць усе віджэты"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Шырына: %1$d, вышыня: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Віджэт \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Стварэнне нататак"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Дадаць"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Дадаць віджэт \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Націсніце, каб змяніць налады віджэта"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Змяніць налады віджэта"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Пошук праграм"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Усталяваць"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Не прапаноўваць праграму"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Замацаваць прапанаваную праграму"</string>
+    <string name="bubble" msgid="3072951361014076670">"Бурбалка"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Стварэнне ярлыкоў"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Дазваляе праграмам дадаваць ярлыкі без умяшання карыстальніка."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"счытваць налады і ярлыкі на галоўным экране"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Адключаная адміністратарам"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дазволіць паварот галоўнага экрана"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Пры павароце тэлефона"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Значкі апавяшчэнняў"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Уключана"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Выкл."</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Усталёўваецца праграма \"<xliff:g id="NAME">%1$s</xliff:g>\", завершана <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Ідзе спампоўка <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> завершана"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> чакае ўсталёўкі"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Праграма \"<xliff:g id="NAME">%1$s</xliff:g>\" знаходзіцца ў архіве. Націсніце, каб спампаваць яе і аднавіць."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Праграма \"<xliff:g id="NAME">%1$s</xliff:g>\" знаходзіцца ў архіве."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"спампаваць і аднавіць"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Неабходна абнавіць праграму"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Гэта версія праграмы састарэла. Абнавіце праграму ўручную, каб зноў карыстацца гэтым ярлыком, або выдаліце значок."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Абнавіць"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Зразумела"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Прыпыніць працоўныя праграмы"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Актываваць"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Фільтр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не ўдалося: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Прыватная прастора"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 643c24e..191ef03 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Приспособленията са деактивирани в безопасния режим"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Няма достъп до прекия път"</string>
     <string name="home_screen" msgid="5629429142036709174">"Начален екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"От настройките задайте <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> като основното приложение за начален екран"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Разделен екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Информация за приложението за %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Настройки за използването на %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нов прозорец"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Запазване на двойката приложения"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Тази двойка приложения не се поддържа на устройството"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Двойката приложения не е налице"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Докоснете и задръжте за преместване на приспособление"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Докоснете двукратно и задръжте за преместване на приспособление или използвайте персонал. действия."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Още опции"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Показв. на всички присп."</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ширина %1$d и височина %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> приспособление"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Водене на бележки"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Добавяне"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Добавяне на приспособлението „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Докоснете, за да промените настройките на приспособлението"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Промяна на настройките на приспособлението"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Търсене в приложенията"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Инсталиране"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Без предлагане на приложение"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Фиксиране на предвиждането"</string>
+    <string name="bubble" msgid="3072951361014076670">"Балонче"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"инсталиране на преки пътища"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Разрешава на приложението да добавя преки пътища без намеса на потребителя."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"четене на настройките и преките пътища на началния екран"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Деактивирано от администратора ви"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Разрешаване на завъртането на началния екран"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"При завъртане на телефона"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Точки за известия"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Вкл."</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Изкл."</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> се инсталира, <xliff:g id="PROGRESS">%2$s</xliff:g> завършено"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> се изтегля. Завършено: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> изчаква инсталиране"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Приложението <xliff:g id="NAME">%1$s</xliff:g> е архивирано. Докоснете за изтегляне и възстановяване."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Приложението <xliff:g id="NAME">%1$s</xliff:g> е архивирано."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"изтегляне и възстановяване"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Изисква се актуализация на приложението"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Приложението за тази икона не е актуализирано. Можете да го актуализирате ръчно, за да активирате отново този пряк път, или да премахнете иконата."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Актуализиране"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Разбрах"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Поставяне на пауза на служебните приложения"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Отмяна на паузата"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Филтър"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Неуспешно: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Частно пространство"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 7e10a0e..43834af 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"সুরক্ষিত মোডে উইজেট নিষ্ক্রিয় থাকে"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"শর্টকাটগুলি অনুপলব্ধ"</string>
     <string name="home_screen" msgid="5629429142036709174">"হোম"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"সেটিংসে গিয়ে <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> অ্যাপকে ডিফল্ট হোম অ্যাপ হিসেবে সেট করুন"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"স্প্লিট স্ক্রিন"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s-এর জন্য অ্যাপ সম্পর্কিত তথ্য"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s-এর জন্য ব্যবহারের সেটিংস"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"নতুন উইন্ডো"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"অ্যাপ পেয়ার সেভ করুন"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"এই ডিভাইসে এই অ্যাপ পেয়ারটি কাজ করে না"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"অ্যাপ পেয়ার উপলভ্য নেই"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"কোনও উইজেট সরাতে সেটি টাচ করে ধরে রাখুন।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"একটি উইজেট সরাতে বা কাস্টম অ্যাকশন ব্যবহার করতে ডবল ট্যাপ করে ধরে রাখুন।"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"আরও বিকল্প"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"সব উইজেট দেখুন"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%2$d উচ্চতা অনুযায়ী %1$d প্রস্থ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>টি উইজেট"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"নোট নেওয়া"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"যোগ করুন"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> উইজেট যোগ করুন"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"উইজেট সেটিংস পরিবর্তন করতে ট্যাপ করুন"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"উইজেট সেটিংস পরিবর্তন করুন"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"অ্যাপ খুঁজুন"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ইনস্টল করুন"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"অ্যাপ সাজেস্ট করবেন না"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"আপনার প্রয়োজন হতে পারে এমন অ্যাপ পিন করুন"</string>
+    <string name="bubble" msgid="3072951361014076670">"বাবল"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"শর্টকাটগুলি ইনস্টল করে"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"একটি অ্যাপ্লিকেশানকে ব্যবহারকারীর হস্তক্ষেপ ছাড়াই শর্টকাটগুলি যোগ করার অনুমতি দেয়৷"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"হোম স্ক্রিনে সেটিংস ও শর্টকাট পড়ুন"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"আপনার প্রশাসক দ্বারা অক্ষম করা হয়েছে"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"হোম স্ক্রিন রোটেট করার অনুমতি দিন"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"যখন ফোনটি ঘোরানো হয়"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"বিজ্ঞপ্তি ডট"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"চালু করা আছে"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"বন্ধ"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ইনস্টল করা হচ্ছে, <xliff:g id="PROGRESS">%2$s</xliff:g> সম্পূর্ণ হয়েছে"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ডাউনলোড হচ্ছে <xliff:g id="PROGRESS">%2$s</xliff:g> সম্পন্ন হয়েছে"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ইনস্টলের অপেক্ষায় রয়েছে"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> আর্কাইভ করা হয়েছে। ডাউনলোড করতে এবং ফিরিয়ে আনতে ট্যাপ করুন।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> আর্কাইভ করা হয়েছে।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ডাউনলোড করুন ও ফিরিয়ে আনুন"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"অ্যাপটি আপডেট করা প্রয়োজন"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"এই আইকনের জন্য অ্যাপটি আপডেট করা নেই। এই শর্টকার্ট আবার চালু করতে, আপনি ম্যানুয়ালি আপডেট করতে বা সরিয়ে দিতে পারবেন।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"আপডেট করুন"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"বুঝেছি"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"অফিসের অ্যাপ পজ করুন"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"আনপজ করুন"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ফিল্টার"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"কাজটি করা যায়নি: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ব্যক্তিগত স্পেস"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 10aa86a..0beeafe 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Vidžeti su onemogućeni u sigurnom načinu rada."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Prečica nije dostupna"</string>
     <string name="home_screen" msgid="5629429142036709174">"Početni ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Postavite <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kao zadanu aplikaciju za početni ekran u Postavkama"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podijeljeni ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Postavke korištenja za: %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Sačuvaj par aplikacija"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Par aplikacija nije podržan na uređaju"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacija nije dostupan"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dodirnite i zadržite da pomjerite vidžet."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvaput dodirnite i zadržite da pomjerite vidžet ili da koristite prilagođene radnje."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Više opcija"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Prikazuj sve vidžete"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Širina %1$d, visina %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pisanje bilješki"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodajte"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodavanje vidžeta <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dodirnite da promijenite postavke vidžeta"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Promjena postavki vidžeta"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pretražite aplikacije"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instaliraj"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne predlaži aplikaciju"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Zakači predviđanje"</string>
+    <string name="bubble" msgid="3072951361014076670">"Oblačić"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instaliraj prečice"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Dopušta aplikaciji dodavanje prečica bez posredovanja korisnika."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"čita postavke na početnom ekranu i prečice"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Onemogućio vaš administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dozvoli rotiranje početnog ekrana"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kada se telefon zarotira"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Tačke za obavještenja"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Uključeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Isključeno"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instaliranje aplikacije <xliff:g id="NAME">%1$s</xliff:g>, završeno je <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> se preuzima, završeno <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> čeka da se instalira"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Arhivirana je aplikacija <xliff:g id="NAME">%1$s</xliff:g>. Dodirnite da je preuzmete i vratite."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Arhivirana je aplikacija <xliff:g id="NAME">%1$s</xliff:g>."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"preuzimanje i vraćanje"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Potrebno je ažurirati aplikaciju"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija za ovu ikonu nije ažurirana. Možete je ažurirati ručno da ponovo omogućite ovu prečicu ili možete ukloniti ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ažuriraj"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Razumijem"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pauziraj poslovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ponovo pokreni"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrirajte"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nije uspjelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privatni prostor"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 3d2ff4a..94a7b41 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"En Mode segur, els widgets estan desactivats."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"La drecera no està disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Inici"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Defineix <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> com a aplicació d\'inici predeterminada a Configuració"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informació de l\'aplicació %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configuració d\'ús de %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Finestra nova"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Desa la parella d\'aplicacions"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Aquesta parella d\'aplicacions no s\'admet en aquest dispositiu"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La parella d\'aplicacions no està disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Fes doble toc i mantén premut per moure un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Fes doble toc i mantén premut per moure un widget o per utilitzar accions personalitzades."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Més opcions"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostra tots els widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d d\'amplada per %2$d d\'alçada"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget de <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Presa de notes"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Afegeix"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Afegeix el widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toca per canviar la configuració del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Canvia la configuració del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Cerca aplicacions"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instal·la"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"No suggereixis l\'aplicació"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fixa la predicció"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bombolla"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instal·la dreceres"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permet que una aplicació afegeixi dreceres sense la intervenció de l\'usuari."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"llegir la configuració i les dreceres de la pantalla d\'inici"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Desactivada per l\'administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permet la rotació de la pantalla d\'inici"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"En girar el telèfon"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Punts de notificació"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activats"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivats"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"S\'està instal·lant <xliff:g id="NAME">%1$s</xliff:g>; s\'ha completat un <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"S\'està baixant <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> completat"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"S\'està esperant per instal·lar <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"L\'aplicació <xliff:g id="NAME">%1$s</xliff:g> està arxivada. Toca per baixar-la i restaurar-la."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"L\'aplicació <xliff:g id="NAME">%1$s</xliff:g> està arxivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"baixa i restaura"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Cal actualitzar l\'aplicació"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"L\'aplicació d\'aquesta icona no està actualitzada. Pots actualitzar-la manualment per tornar a activar aquesta drecera o pots suprimir la icona."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualitza"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entesos"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Posa en pausa les aplicacions de treball"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Reactiva"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtra"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Error: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espai privat"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 14a3583..93017bf 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"V nouzovém režimu jsou widgety zakázány."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Zkratka není k dispozici"</string>
     <string name="home_screen" msgid="5629429142036709174">"Domů"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Nastavit <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> jako výchozí vstupní aplikaci v Nastavení"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Rozdělit obrazovku"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informace o aplikaci %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nastavení využití pro aplikaci %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nové okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Uložit dvojici aplikací"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Tento pár aplikací není na tomto zařízení podporován"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Dvojice aplikací není k dispozici"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Widget přesunete klepnutím a podržením."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvojitým klepnutím a podržením přesunete widget, případně použijte vlastní akce."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Další možnosti"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Zobrazit všechny widgety"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"šířka %1$d, výška %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Psaní poznámek"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Přidat"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Přidat widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Klepnutím změníte nastavení widgetu"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Změnit nastavení widgetu"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Hledat v aplikacích"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Nainstalovat"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Nenavrhovat aplikaci"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Připnout předpověď"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bublat"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalace zástupce"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Umožňuje aplikaci přidat zástupce bez zásahu uživatele."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"čtení nastavení a zkratek plochy"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Zakázáno administrátorem"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Povolit otáčení plochy"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Při otočení telefonu"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puntíky s oznámením"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Zapnuto"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Vypnuto"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalace aplikace <xliff:g id="NAME">%1$s</xliff:g>, dokončeno <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Stahování aplikace <xliff:g id="NAME">%1$s</xliff:g> (dokončeno <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Instalace aplikace <xliff:g id="NAME">%1$s</xliff:g> čeká na zahájení"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikace <xliff:g id="NAME">%1$s</xliff:g> je archivována. Klepnutím ji můžete stáhnout a obnovit."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikace <xliff:g id="NAME">%1$s</xliff:g> je archivována."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"stáhnout a obnovit"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Je nutná aktualizace aplikace"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikace pro tuto ikonu není nainstalována. Můžete ji ručně aktualizovat, aby zkratka znovu fungovala, případně můžete ikonu odstranit."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualizovat"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pozastavit pracovní aplikace"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Zrušit pozastavení"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtr"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Selhalo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Soukromý prostor"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index f18e1e8..c22d0dc 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets er deaktiveret i Beskyttet tilstand"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Genvejen er ikke tilgængelig"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startskærm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Angiv <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> som standardstartapp i Indstillinger"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Opdel skærm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Appinfo for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Indstillinger for brug af %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nyt vindue"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Gem appsammenknytning"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Denne appsammenknytning understøttes ikke på enheden"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Appsammenknytning er ikke tilgængelig"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Hold en widget nede for at flytte den."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Tryk to gange, og hold en widget nede for at flytte den eller bruge tilpassede handlinger."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Flere valgmuligheder"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Vis alle widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d i bredden og %2$d i højden"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widgetten <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notetagning"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Tilføj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Tilføj <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tryk for at ændre widgetindstillinger"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Skift widgetindstillinger"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Søg efter apps"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installer"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Foreslå ikke en app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fastgør forslaget"</string>
+    <string name="bubble" msgid="3072951361014076670">"Boble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installere genveje"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Tillader, at en app tilføjer genveje uden brugerens indgriben."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"læs indstillinger og genveje for startskærm"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Deaktiveret af din administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Tillad rotation af startskærmen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Når telefonen roteres"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notifikationsprikker"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Til"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Fra"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeres. <xliff:g id="PROGRESS">%2$s</xliff:g> fuldført"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloades. <xliff:g id="PROGRESS">%2$s</xliff:g> er gennemført"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> venter på at installere"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> er arkiveret Tryk for at downloade og gendanne."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> er arkiveret."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download og gendan"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Appen skal opdateres"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Appen, der tilhører dette ikon, er ikke opdateret. Du kan opdatere appen manuelt for at genaktivere denne genvej, eller du kan fjerne ikonet."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Opdater"</string>
@@ -171,7 +185,7 @@
     <string name="action_deep_shortcut" msgid="2864038805849372848">"Genveje"</string>
     <string name="action_dismiss_notification" msgid="5909461085055959187">"Afvis"</string>
     <string name="accessibility_close" msgid="2277148124685870734">"Luk"</string>
-    <string name="all_apps_personal_tab" msgid="4190252696685155002">"Personlige"</string>
+    <string name="all_apps_personal_tab" msgid="4190252696685155002">"Personlig"</string>
     <string name="all_apps_work_tab" msgid="4884822796154055118">"Arbejde"</string>
     <string name="work_profile_toggle_label" msgid="3081029915775481146">"Arbejdsprofil"</string>
     <string name="work_profile_edu_work_apps" msgid="7895468576497746520">"Arbejdsapps har badges og kan ses af din it-administrator"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Sæt arbejdsapps på pause"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Genoptag"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mislykket: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privat område"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 9a4e5e2..21ada28 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -27,18 +27,20 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets im abgesicherten Modus deaktiviert"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Verknüpfung nicht verfügbar"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startbildschirm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> in den Einstellungen als Stand-Start-App festlegen"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Splitscreen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App-Info für %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nutzungseinstellungen für %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Neues Fenster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"App-Paar speichern"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Dieses App-Paar wird auf diesem Gerät nicht unterstützt"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Gerät aufklappen, um dieses App-Paar zu verwenden"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"App-Paar nicht verfügbar"</string>
-    <string name="long_press_widget_to_add" msgid="3587712543577675817">"Zum Verschieben des Widgets berühren und halten"</string>
+    <string name="long_press_widget_to_add" msgid="3587712543577675817">"Zum Verschieben des Widgets gedrückt halten"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Doppeltippen und halten, um ein Widget zu bewegen oder benutzerdefinierte Aktionen zu nutzen."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Weitere Optionen"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Alle Widgets anzeigen"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d breit und %2$d hoch"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notizen"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Hinzufügen"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Widget „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“ hinzufügen"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tippen, um die Widget-Einstellungen zu ändern"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Widget-Einstellungen ändern"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Apps finden"</string>
@@ -75,7 +83,7 @@
     <string name="label_application" msgid="8531721983832654978">"App"</string>
     <string name="all_apps_label" msgid="5015784846527570951">"Alle Apps"</string>
     <string name="notifications_header" msgid="1404149926117359025">"Benachrichtigungen"</string>
-    <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Zum Verschieben einer Verknüpfung berühren und halten"</string>
+    <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Zum Verschieben einer Verknüpfung gedrückt halten"</string>
     <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"Doppeltippen und halten, um eine Verknüpfung zu bewegen oder benutzerdefinierte Aktionen zu nutzen."</string>
     <string name="out_of_space" msgid="6455557115204099579">"Auf diesem Startbildschirm ist kein Platz mehr vorhanden"</string>
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"Ablage \"Favoriten\" ist voll."</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installieren"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"App nicht vorschlagen"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Vorgeschlagene App fixieren"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Verknüpfungen installieren"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Ermöglicht einer App das Hinzufügen von Verknüpfungen ohne Eingreifen des Nutzers"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"Einstellungen und Verknüpfungen auf dem Startbildschirm lesen"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Von deinem Administrator deaktiviert"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Drehen des Startbildschirms zulassen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Beim Drehen des Smartphones"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"App-Benachrichtigungspunkte"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"An"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Aus"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> wird installiert, <xliff:g id="PROGRESS">%2$s</xliff:g> abgeschlossen"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> wird heruntergeladen, <xliff:g id="PROGRESS">%2$s</xliff:g> abgeschlossen"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Warten auf Installation von <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ist archiviert. Tippe, um die App herunterzuladen und wiederherzustellen."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ist archiviert."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"herunterladen und wiederherstellen"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App-Update erforderlich"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Die App für dieses Symbol wurde noch nicht aktualisiert. Du kannst sie manuell aktualisieren, um die Verknüpfung wieder zu aktivieren, oder das Symbol entfernen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualisieren"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Geschäftliche Apps pausieren"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Nicht mehr pausieren"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Fehler: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Vertrauliches Profil"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 840ff5b..1e18977 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Τα γραφικά στοιχεία απενεργοποιήθηκαν στην ασφαλή λειτουργία"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Η συντόμευση δεν είναι διαθέσιμη"</string>
     <string name="home_screen" msgid="5629429142036709174">"Αρχική οθόνη"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ορίστε το <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ως την προεπιλεγμένη εφαρμογή αρχικής οθόνης στις Ρυθμίσεις"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Διαχωρισμός οθόνης"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Πληροφορίες εφαρμογής για %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Ρυθμίσεις χρήσης για %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Νέο παράθυρο"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Αποθήκευση ζεύγους εφαρμογών"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Αυτό το ζεύγος εφαρμογών δεν υποστηρίζεται σε αυτή τη συσκευή"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Το ζεύγος εφαρμογών δεν είναι διαθέσιμο"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Πατήστε παρατετ. για μετακίνηση γραφ. στοιχείου."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Πατήστε δύο φορές παρατεταμένα για μετακίνηση γραφικού στοιχείου ή χρήση προσαρμοσμένων ενεργειών."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Περισσότερες επιλογές"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Εμφ. συνόλου γραφ. στοιχ."</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Πλάτος %1$d επί ύψος %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Γραφικό στοιχείο <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Δημιουργία σημειώσεων"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Προσθήκη"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Προσθήκη του γραφικού στοιχείου <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Πατήστε για αλλαγή των ρυθμίσεων του γραφικού στοιχείου"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Αλλαγή ρυθμίσεων γραφικού στοιχείου"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Αναζήτηση εφαρμογών"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Εγκατάσταση"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Να μην προτείνεται"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Καρφίτσωμα πρόβλεψης"</string>
+    <string name="bubble" msgid="3072951361014076670">"Συννεφάκι"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"εγκατάσταση συντομεύσεων"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Επιτρέπει σε μια εφαρμογή την προσθήκη συντομεύσεων χωρίς την παρέμβαση του χρήστη."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ανάγνωση ρυθμίσεων και συντομεύσεων αρχικής οθόνης"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Απενεργοποιήθηκε από τον διαχειριστή σας"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Να επιτρέπεται η περιστροφή της αρχικής οθόνης"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Όταν το τηλέφωνο περιστρέφεται"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Κουκκίδες ειδοποίησης"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ενεργοποίηση"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Απενεργοποίηση"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Έχει ολοκληρωθεί το <xliff:g id="PROGRESS">%2$s</xliff:g> της εγκατάστασης της εφαρμογής <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Λήψη <xliff:g id="NAME">%1$s</xliff:g>, ολοκληρώθηκε <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> σε αναμονή για εγκατάσταση"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Η εφαρμογή <xliff:g id="NAME">%1$s</xliff:g> είναι αρχειοθετημένη. Πατήστε για λήψη και επαναφορά."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Η εφαρμογή <xliff:g id="NAME">%1$s</xliff:g> είναι αρχειοθετημένη."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"λήψη και επαναφορά"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Απαιτείται ενημέρωση της εφαρμογής"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Η εφαρμογή για αυτό το εικονίδιο δεν έχει ενημερωθεί. Μπορείτε να την ενημερώσετε μη αυτόματα για να ενεργοποιήσετε ξανά τη συγκεκριμένη συντόμευση ή να καταργήσετε το εικονίδιο."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ενημέρωση"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Το κατάλαβα"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Παύση εφαρμογών εργασιών"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Αναίρεση παύσης"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Φίλτρο"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Αποτυχία: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Ιδιωτικός χώρος"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 4deacb6..a2a4145 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as the default home app in Settings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap &amp; hold to move a widget or use custom actions."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"More options"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Show all widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Install"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Don\'t suggest app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Pin prediction"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"install shortcuts"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Allows an app to add shortcuts without user intervention."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"read Home settings and shortcuts"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 08ff6e7..c600680 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New Window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
@@ -38,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap and hold to move a widget or use custom actions."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"More options"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Show all widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -66,6 +69,9 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Show all"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Show all widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Showing all widgets"</string>
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -90,6 +96,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Install"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Don\'t suggest app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Pin Prediction"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"install shortcuts"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Allows an app to add shortcuts without user intervention."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"read home settings and shortcuts"</string>
@@ -120,6 +127,8 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Landscape mode"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Set phone into landscape mode"</string>
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -138,7 +147,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut, or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -183,6 +193,7 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Got it"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Work apps schedule"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 4deacb6..a2a4145 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as the default home app in Settings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap &amp; hold to move a widget or use custom actions."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"More options"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Show all widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Install"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Don\'t suggest app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Pin prediction"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"install shortcuts"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Allows an app to add shortcuts without user intervention."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"read Home settings and shortcuts"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 4deacb6..a2a4145 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as the default home app in Settings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap &amp; hold to move a widget or use custom actions."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"More options"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Show all widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Install"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Don\'t suggest app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Pin prediction"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"install shortcuts"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Allows an app to add shortcuts without user intervention."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"read Home settings and shortcuts"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index fa6d1f1..769567c 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‎‏‏‎‎‏‎‏‎‎‏‎‏‏‎‎‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‏‎‏‎‎‏‏‏‏‏‏‎Split screen‎‏‎‎‏‎"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‎‏‏‏‎‏‏‏‏‏‏‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‏‎‏‎‎‎‎‏‎‎‎‏‎‏‎‎‎App info for %1$s‎‏‎‎‏‎"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‎‎‏‏‏‎‎‎‎‎‏‎‏‎‎‎‎‏‎‎‎‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‎‎‎‏‎‏‏‏‏‎‏‎Usage settings for %1$s‎‏‎‎‏‎"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‏‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‏‏‏‎‏‏‏‏‎‏‎‏‎‎‎‏‎‎‏‎‏‎‏‎‏‏‎New Window‎‏‎‎‏‎"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‎‎‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‏‎‎‏‏‎‎‎‎‏‏‏‎‎‎‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‎Save app pair‎‏‎‎‏‎"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‎‎‎‏‏‏‎‎‏‎‏‎‏‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‏‎‎‎‎‏‏‏‎‏‏‏‏‎‏‎‏‏‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎<xliff:g id="APP1">%1$s</xliff:g>‎‏‎‎‏‏‏‎ | ‎‏‎‎‏‏‎<xliff:g id="APP2">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‏‎‎‏‎‏‏‎‏‏‎‏‎‏‏‏‏‎‎‏‏‎‏‎‎‎‎‏‎‎‏‏‎‎‎‎‎‎‎This app pair isn\'t supported on this device‎‏‎‎‏‎"</string>
@@ -38,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‏‎‏‏‏‎‎‎‎‏‎‏‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‎‎‏‎‎‎‎‎‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‏‏‏‏‏‎App pair isn\'t available‎‏‎‎‏‎"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‎‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‎Touch &amp; hold to move a widget.‎‏‎‎‏‎"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‏‎‏‏‏‏‏‎‏‎‏‎‎‎‎‎‎‎‏‏‏‏‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‎Double-tap &amp; hold to move a widget or use custom actions.‎‏‎‎‏‎"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‏‏‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‎‏‎‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎‏‏‎More options‎‏‎‎‏‎"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‏‏‎‎‏‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‎‎‎‎‏‏‏‎‏‎‏‏‏‎Show all widgets‎‏‎‎‏‎"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‎‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎‏‏‏‏‏‎%1$d × %2$d‎‏‎‎‏‎"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‎‎‎‏‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎‎‎‎‎‏‏‏‏‎‎%1$d wide by %2$d high‎‏‎‎‏‎"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‏‏‏‎‏‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="WIDGET_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ widget‎‏‎‎‏‎"</string>
@@ -90,6 +93,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‏‏‏‏‎‎‏‎‏‎‏‏‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‏‎‎‎‏‎‏‏‎‎‎‏‏‎‏‎Install‎‏‎‎‏‎"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‎‏‎‎‏‏‎‎‎‎‏‏‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‏‎‏‎‎‏‎‎Don\'t suggest app‎‏‎‎‏‎"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‏‏‏‏‎‎‏‎‏‏‎‎‎‎‏‏‎‎‎‏‏‏‎‎‎‎‎‏‏‎‎‎‎‏‏‎‎‏‎‎‏‏‏‎‎‏‎‏‎‏‎‎‏‎‎Pin Prediction‎‏‎‎‏‎"</string>
+    <string name="bubble" msgid="3072951361014076670">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‎‏‎‏‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‏‏‎‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‏‏‏‏‏‏‎‎Bubble‎‏‎‎‏‎"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‎‏‎‎‏‏‎‎‎‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎install shortcuts‎‏‎‎‏‎"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‏‏‎‎‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‏‎‎‏‏‏‎‎‎‎‎‎‏‏‎Allows an app to add shortcuts without user intervention.‎‏‎‎‏‎"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‏‏‏‎‏‏‏‎‏‏‏‏‎‏‏‎read home settings and shortcuts‎‏‎‎‏‎"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index e734744..44147c2 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets inhabilitados en modo seguro"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"El acceso directo no está disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Pantalla principal"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Establece <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como la app de inicio predeterminada en Configuración"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Información de la app de %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configuración del uso de %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Ventana nueva"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar vinculación"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"No se admite esta vinculación de apps en este dispositivo"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La vinculación de apps no está disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Mantén presionado para mover un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Presiona dos veces y mantén presionado para mover un widget o usar acciones personalizadas."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Más opciones"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostrar todos los widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de ancho por %2$d de alto"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Tomar notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Agregar"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Agregar widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Presiona para cambiar la configuración del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Cambiar la configuración del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Buscar apps"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalar"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"No sugerir app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fijar predicción"</string>
+    <string name="bubble" msgid="3072951361014076670">"Burbuja"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalar accesos directos"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permite que una aplicación agregue accesos directos sin que el usuario intervenga."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"leer parámetros de configuración y accesos directos de la página principal"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"El administrador inhabilitó esta función"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir la rotación de la pantalla principal"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Al girar el teléfono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puntos de notificación"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activados"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivados"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Se está instalando <xliff:g id="NAME">%1$s</xliff:g>; <xliff:g id="PROGRESS">%2$s</xliff:g> completado"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Se completó el <xliff:g id="PROGRESS">%2$s</xliff:g> de la descarga de <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Instalación de <xliff:g id="NAME">%1$s</xliff:g> en espera"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> está archivada. Presiona para descargar y restablecer."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> está archivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descargar y restablecer"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Es necesario actualizar la app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"No se actualizó la app de este ícono. Puedes actualizarla manualmente para rehabilitar el acceso directo, o bien quitar el ícono."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizar"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entendido"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Detener apps de trabajo"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Reanudar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtro"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Error: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espacio privado"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 3e0e516..40e04d5 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets inhabilitados en modo Seguro"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Acceso directo no disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Inicio"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Define <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como aplicación de inicio predeterminada en Ajustes"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Información de la aplicación %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Ajustes de uso para %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Ventana nueva"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar apps emparejadas"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"El dispositivo no admite esta aplicación emparejada"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La aplicación emparejada no está disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Mantén pulsado un widget para moverlo"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toca dos veces y mantén pulsado un widget para moverlo o usar acciones personalizadas."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Más opciones"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostrar todos los widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de ancho por %2$d de alto"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget de <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Toma de notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Añadir"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Añadir widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toca para cambiar los ajustes del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Cambiar ajustes del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Buscar aplicaciones"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalar"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"No sugerir aplicación"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fijar predicción"</string>
+    <string name="bubble" msgid="3072951361014076670">"Burbuja"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalar accesos directos"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permite que una aplicación añada accesos directos sin intervención del usuario."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"leer ajustes y accesos directos de la pantalla de inicio"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Inhabilitado por el administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir rotación de la pantalla de inicio"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Al girar el teléfono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Burbujas de notificación"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activado"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivadas"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalando <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> completado"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Descargando <xliff:g id="NAME">%1$s</xliff:g> (<xliff:g id="PROGRESS">%2$s</xliff:g> completado)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Esperando para instalar <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> está archivada. Toca para descargar y restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> está archivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descargar y restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Debes actualizar la aplicación"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"La aplicación de este icono no está actualizada. Puedes actualizarla manualmente para volver a habilitar este acceso directo o puedes eliminar el icono."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizar"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entendido"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausar aplicaciones de trabajo"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Reanudar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtro"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Se ha producido un error: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espacio privado"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index bab7da3..de62de1 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Turvarežiimis on vidinad keelatud"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Otsetee pole saadaval"</string>
     <string name="home_screen" msgid="5629429142036709174">"Avakuva"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Määrake rakendus <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> seadetes avakuva vaikerakenduseks"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Jagatud ekraanikuva"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Rakenduse teave: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Kasutuse seaded: %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Uus aken"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salvesta rakendusepaar"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"See rakendusepaar ei ole selles seadmes toetatud"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Rakendusepaar ei ole saadaval"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Vidina teisaldamiseks puudutage ja hoidke all."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Vidina teisaldamiseks või kohandatud toimingute kasutamiseks topeltpuudutage ja hoidke all."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Rohkem valikuid"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Kuva kõik vidinad"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d lai ja %2$d kõrge"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Vidin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Märkmete tegemine"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Lisa"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Lisa vidin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Puudutage vidina seadete muutmiseks"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Vidina seadete muutmine"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Otsige rakendusi"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installimine"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ära soovita rakendust"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Kinnita ennustus"</string>
+    <string name="bubble" msgid="3072951361014076670">"Mull"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installi otseteed"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Võimaldab rakendusel lisada otseteid kasutaja sekkumiseta."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"avakuva seadete ja otseteede lugemine"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Keelas administraator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Luba avakuva pööramine"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kui telefoni pööratakse"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Märguandetäpid"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Sees"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Väljas"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Üksust <xliff:g id="NAME">%1$s</xliff:g> installitakse, <xliff:g id="PROGRESS">%2$s</xliff:g> on valmis"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Rakenduse <xliff:g id="NAME">%1$s</xliff:g> allalaadimine, <xliff:g id="PROGRESS">%2$s</xliff:g> on valmis"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> on installimise ootel"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> on arhiivitud. Puudutage allalaadimiseks ja taastamiseks."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> on arhiivitud."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"laadi alla ja taasta"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Rakendust tuleb värskendada"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Selle ikooni rakendust pole värskendatud. Otsetee uuesti lubamiseks võite rakendust käsitsi värskendada või ikooni eemaldada."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Värskenda"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Selge"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Peata töörakendused"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Lõpeta peatamine"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nurjus: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privaatne ruum"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index b8017d6..27510b3 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -27,18 +27,20 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgetak desgaitu egin dira modu seguruan"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Lasterbideak ez daude erabilgarri"</string>
     <string name="home_screen" msgid="5629429142036709174">"Orri nagusia"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ezarri <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> hasierako aplikazio lehenetsi gisa ezarpenetan"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantaila zatitzea"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s aplikazioari buruzko informazioa"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s aplikazioaren erabilera-ezarpenak"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Leiho berria"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Gorde aplikazio parea"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
-    <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Aplikazio pare hori ez da onartzen gailu honetan"</string>
+    <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Aplikazio pare hori ez da gailu honekin bateragarria"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Zabaldu gailua aplikazio pare hau erabiltzeko"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"Aplikazio parea ez dago erabilgarri"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Eduki sakatuta widget bat mugitzeko."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Sakatu birritan eta eduki sakatuta widget bat mugitzeko edo ekintza pertsonalizatuak erabiltzeko."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Aukera gehiago"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Erakutsi widget guztiak"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d zabal eta %2$d luze"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widgeta"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Oharrak idazteko"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Gehitu"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Gehitu <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widgeta"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Sakatu hau widgeten ezarpenak aldatzeko"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Aldatu widgeten ezarpenak"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Bilatu aplikazioetan"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalatu"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ez iradoki aplikazioa"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Ainguratu iragarpena"</string>
+    <string name="bubble" msgid="3072951361014076670">"Burbuila"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Instalatu lasterbideak"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Erabiltzaileak ezer egin gabe lasterbideak gehitzeko baimena ematen die aplikazioei."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"irakurri hasierako pantailako ezarpenak eta lasterbideak"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administratzaileak desgaitu du"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Eman orri nagusia biratzeko baimena"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefonoa biratzean"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Jakinarazpen-biribiltxoak"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktibatuta"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desaktibatuta"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> instalatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> deskargatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> instalatzeko zain"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> artxibatuta dago. Sakatu deskargatzeko eta leheneratzeko."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> artxibatuta dago."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"deskargatu eta leheneratu"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Aplikazioa eguneratu egin behar da"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Ikonoaren aplikazioa ez dago eguneratuta. Lasterbidea berriro gaitzeko, eskuz egunera dezakezu aplikazioa. Bestela, kendu ikonoa."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Eguneratu"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ados"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausatu laneko aplikazioak"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Aktibatu berriro"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Iragazi"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Huts egin du: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Eremu pribatua"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 5cbce53..8a21dc7 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -24,51 +24,59 @@
     <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>
+    <string name="safemode_widget_error" msgid="4863470563535682004">"ابزاره‌ها در حالت ایمن غیرفعال هستند"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"میان‌بر دردسترس نیست"</string>
     <string name="home_screen" msgid="5629429142036709174">"صفحه اصلی"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"تنظیم <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> به‌عنوان برنامه صفحه اصلی پیش‌فرض در «تنظیمات»"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"صفحهٔ دونیمه"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏اطلاعات برنامه %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏تنظیمات مصرف برای %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"پنجره جدید"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ذخیره جفت برنامه"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"از این جفت برنامه در این دستگاه پشتیبانی نمی‌شود"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"برای استفاده از این جفت برنامه، دستگاه را باز کنید"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"جفت برنامه دردسترس نیست"</string>
-    <string name="long_press_widget_to_add" msgid="3587712543577675817">"برای جابه‌جا کردن ابزارک، لمس کنید و نگه دارید."</string>
-    <string name="long_accessible_way_to_add" msgid="2733588281439571974">"برای جابه‌جا کردن ابزارک یا استفاده از کنش‌های سفارشی، دو تک‌ضرب بزنید و نگه دارید."</string>
+    <string name="long_press_widget_to_add" msgid="3587712543577675817">"برای جابه‌جا کردن ابزاره، لمس کنید و نگه دارید."</string>
+    <string name="long_accessible_way_to_add" msgid="2733588281439571974">"برای جابه‌جا کردن ابزاره یا استفاده از کنش‌های سفارشی، دو تک‌ضرب بزنید و نگه دارید."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"گزینه‌های بیشتر"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"نمایش همه ابزاره‌ها"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏%1$d عرض در %2$d طول"</string>
-    <string name="widget_preview_context_description" msgid="9045841361655787574">"ابزارک <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
-    <string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"‏ابزارک <xliff:g id="WIDGET_NAME">%1$s</xliff:g>، %2$d عرض در %3$d ارتفاع"</string>
-    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"ابزارک را لمس کنید و نگه دارید تا بتوانید آن را در صفحه اصلی حرکت دهید"</string>
+    <string name="widget_preview_context_description" msgid="9045841361655787574">"ابزاره <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"‏ابزاره <xliff:g id="WIDGET_NAME">%1$s</xliff:g>، %2$d عرض در %3$d ارتفاع"</string>
+    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"ابزاره را لمس کنید و نگه دارید تا بتوانید آن را در صفحه اصلی حرکت دهید"</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"افزودن به صفحه اصلی"</string>
-    <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"ابزارک <xliff:g id="WIDGET_NAME">%1$s</xliff:g> به صفحه اصلی اضافه شد"</string>
+    <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"ابزاره <xliff:g id="WIDGET_NAME">%1$s</xliff:g> به صفحه اصلی اضافه شد"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"پیشنهادها"</string>
-    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"بایدها"</string>
+    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"ضروریات"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"اخبار و مجله"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"سرگرمی"</string>
     <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"اجتماعی"</string>
     <string name="others_widget_recommendation_category_label" msgid="5555987036267226245">"پیشنهاداتی برای شما"</string>
-    <string name="widget_picker_right_pane_accessibility_title" msgid="1673313931455067502">"ابزارک‌های <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> در سمت چپ، جستجو و گزینه‌ها در سمت راست"</string>
-    <string name="widgets_count" msgid="6467746476364652096">"{count,plural, =1{‏# ابزارک}one{‏# ابزارک}other{‏# ابزارک}}"</string>
+    <string name="widget_picker_right_pane_accessibility_title" msgid="1673313931455067502">"ابزاره‌های <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> در سمت چپ، «جستجو» و گزینه‌ها در سمت راست"</string>
+    <string name="widgets_count" msgid="6467746476364652096">"{count,plural, =1{‏# ابزاره}one{‏# ابزاره}other{‏# ابزاره}}"</string>
     <string name="shortcuts_count" msgid="8471715556199592381">"{count,plural, =1{‏# میان‌بر}one{‏# میان‌بر}other{‏# میان‌بر}}"</string>
     <string name="widgets_and_shortcuts_count" msgid="7209136747878365116">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>،<xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
-    <string name="widget_button_text" msgid="2880537293434387943">"ابزارک‌ها"</string>
+    <string name="widget_button_text" msgid="2880537293434387943">"ابزاره‌ها"</string>
     <string name="widgets_full_sheet_search_bar_hint" msgid="8484659090860596457">"جستجو"</string>
     <string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"پاک کردن نوشتار از چارگوش جستجو"</string>
-    <string name="no_widgets_available" msgid="4337693382501046170">"ابزارک و میان‌بری دردسترس نیست"</string>
-    <string name="no_search_results" msgid="3787956167293097509">"هیچ ابزارک یا میان‌بری پیدا نشد"</string>
-    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"ابزارک‌های شخصی"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"کار"</string>
+    <string name="no_widgets_available" msgid="4337693382501046170">"ابزاره و میان‌بری دردسترس نیست"</string>
+    <string name="no_search_results" msgid="3787956167293097509">"هیچ ابزاره یا میان‌بری پیدا نشد"</string>
+    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"ابزاره‌های شخصی"</string>
+    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ابزاره‌های کاری"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"مکالمه‌ها"</string>
     <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="reconfigurable_widget_education_tip" msgid="6336962690888067057">"برای تغییر تنظیمات ابزارک، تک‌ضرب بزنید"</string>
-    <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغییر تنظیمات ابزارک"</string>
+    <string name="widget_add_button_content_description" msgid="1810530016360039643">"افزودن ابزاره <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
+    <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"برای تغییر تنظیمات ابزاره، تک‌ضرب بزنید"</string>
+    <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغییر تنظیمات ابزاره"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"جستجوی برنامه‌ها"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"درحال بارگیری برنامه‌‌ها…"</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"هیچ برنامه‌ای در مطابقت با «<xliff:g id="QUERY">%1$s</xliff:g>» پیدا نشد"</string>
@@ -91,15 +99,16 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"نصب"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"برنامه پیشنهاد داده نشود"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"سنجاق کردن پیشنهاد"</string>
+    <string name="bubble" msgid="3072951361014076670">"حبابک"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"نصب میان‌برها"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"به برنامه اجازه می‌دهد میان‌برها را بدون دخالت کاربر اضافه کند."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"خواندن تنظیمات و میان‌برهای صفحه اصلی"</string>
     <string name="permdesc_read_settings" msgid="4208061150510996676">"به برنامه اجازه می‌دهد تنظیمات و میان‌برهای صفحه اصلی را بخواند."</string>
     <string name="permlab_write_settings" msgid="4820028712156303762">"نوشتن تنظیمات و میان‌برهای صفحه اصلی"</string>
     <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_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="folder_hint_text" msgid="5174843001373488816">"ویرایش نام"</string>
     <string name="disabled_app_label" msgid="6673129024321402780">"<xliff:g id="APP_NAME">%1$s</xliff:g> غیرفعال شد"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"توسط سرپرست سیستم غیرفعال شده است"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"مجاز کردن چرخش صفحه اصلی"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"وقتی تلفن چرخانده می‌شود"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"نقطه‌های اعلان"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"روشن"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"خاموش"</string>
@@ -128,7 +141,7 @@
     <string name="msg_missing_notification_access" msgid="281113995110910548">"برای نمایش «نقطه‌های اعلان»، اعلان‌های برنامه را برای <xliff:g id="NAME">%1$s</xliff:g> روشن کنید"</string>
     <string name="title_change_settings" msgid="1376365968844349552">"تغییر تنظیمات"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"نمایش نقطه‌های اعلان"</string>
-    <string name="developer_options_title" msgid="700788437593726194">"گزینه‌های برنامه‌نویس"</string>
+    <string name="developer_options_title" msgid="700788437593726194">"گزینه‌های توسعه‌دهندگان"</string>
     <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"افزودن نماد برنامه‌ها به صفحه اصلی"</string>
     <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"برای برنامه‌های جدید"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"نامشخص"</string>
@@ -139,13 +152,14 @@
     <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>
     <string name="dialog_remove" msgid="6510806469849709407">"برداشتن"</string>
-    <string name="widgets_list" msgid="796804551140113767">"فهرست ابزارک‌ها"</string>
-    <string name="widgets_list_closed" msgid="6141506579418771922">"فهرست ابزارک‌ها بسته شد"</string>
+    <string name="widgets_list" msgid="796804551140113767">"فهرست ابزاره‌ها"</string>
+    <string name="widgets_list_closed" msgid="6141506579418771922">"فهرست ابزاره‌ها بسته شد"</string>
     <string name="action_add_to_workspace" msgid="215894119683164916">"افزودن به صفحه اصلی"</string>
     <string name="action_move_here" msgid="2170188780612570250">"انتقال مورد به اینجا"</string>
     <string name="item_added_to_workspace" msgid="4211073925752213539">"مورد به صفحه اصلی اضافه شد"</string>
@@ -167,7 +181,7 @@
     <string name="action_increase_height" msgid="459390020612501122">"افزایش ارتفاع"</string>
     <string name="action_decrease_width" msgid="1374549771083094654">"کاهش عرض"</string>
     <string name="action_decrease_height" msgid="282377193880900022">"کاهش ارتفاع"</string>
-    <string name="widget_resized" msgid="9130327887929620">"اندازه ابزارک به عرض <xliff:g id="NUMBER_0">%1$s</xliff:g> ارتفاع <xliff:g id="NUMBER_1">%2$s</xliff:g> تغییر کرد"</string>
+    <string name="widget_resized" msgid="9130327887929620">"اندازه ابزاره به عرض <xliff:g id="NUMBER_0">%1$s</xliff:g> ارتفاع <xliff:g id="NUMBER_1">%2$s</xliff:g> تغییر کرد"</string>
     <string name="action_deep_shortcut" msgid="2864038805849372848">"میان‌برها"</string>
     <string name="action_dismiss_notification" msgid="5909461085055959187">"رد کردن"</string>
     <string name="accessibility_close" msgid="2277148124685870734">"بستن"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"متوجه‌ام"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"توقف موقت برنامه‌های کاری"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ازسرگیری"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"فیلتر"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ناموفق بود: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"فضای خصوصی"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 181c89e..c778a61 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgetit poistettu käytöstä vikasietotilassa"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Pikakuvake ei ole käytettävissä."</string>
     <string name="home_screen" msgid="5629429142036709174">"Etusivu"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Valitse <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> oletusaloitusnäyttösovellukseksi asetuksissa"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Jaettu näyttö"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Sovellustiedot: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Käyttöasetus tälle: %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Uusi ikkuna"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Tallenna sovelluspari"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Sovellusparia ei tueta tällä laitteella"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Sovelluspari ei ole saatavilla"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Kosketa pitkään, niin voit siirtää widgetiä."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Kaksoisnapauta ja paina pitkään, niin voit siirtää widgetiä tai käyttää muokattuja toimintoja."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Lisäasetukset"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Näytä kaikki widgetit"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Leveys: %1$d, korkeus: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Muistiinpanojen tekeminen"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Lisää"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Lisää widget: <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Napauta, niin voit muuttaa widgetin asetuksia"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Muuta widgetin asetuksia"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Hae sovelluksia"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Asenna"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Älä ehdota sovellusta"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Kiinnitä sovellus"</string>
+    <string name="bubble" msgid="3072951361014076670">"Kupla"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"asenna pikakuvakkeita"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Antaa sovelluksen lisätä pikakuvakkeita itsenäisesti ilman käyttäjän valintaa."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"lukea aloitusnäytön asetuksia ja pikakuvakkeita"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Järjestelmänvalvoja on poistanut toiminnon käytöstä."</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Salli aloitusnäytön kiertäminen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kun puhelinta kierretään"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pistemerkit"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Päällä"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Ei päällä"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> asennetaan, <xliff:g id="PROGRESS">%2$s</xliff:g> valmis"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> latautuu, valmiina <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> odottaa asennusta"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> on arkistoitu. Lataa ja palauta napauttamalla."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> on arkistoitu."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"lataa ja palauta"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Sovelluspäivitys vaaditaan"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Kuvakkeen sovellusta ei ole päivitetty. Voit ottaa pikakuvakkeen uudelleen käyttöön päivittämällä sovelluksen tai poistaa kuvakkeen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Päivitä"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Keskeytä työsovellusten käyttö"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Jatka"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Suodatin"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Epäonnistui: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Yksityinen tila"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index f95f26d..1f5e264 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets désactivés en mode sans échec"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Le raccourci n\'est pas disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Accueil"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Définissez <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> comme appli d\'accueil par défaut dans les paramètres"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Écran divisé"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Renseignements sur l\'appli pour %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Paramètres d\'utilisation pour %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nouvelle fenêtre"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Enr. paire d\'applis"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applis n\'est pas prise en charge sur cet appareil"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La Paire d\'applis n\'est pas offerte"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Maintenez le doigt sur un widget pour le déplacer."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Touchez 2x un widget et maintenez le doigt dessus pour le déplacer ou utiliser des actions personnalisées."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Autres options"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Afficher tous les widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largeur sur %2$d de hauteur"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Prise de note"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ajouter"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Ajoutez le widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Touchez pour modifier les paramètres du widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifier les paramètres du widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Rechercher dans les applis"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installer"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne pas suggérer d\'appli"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Épingler la prédiction"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bulle"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installer des raccourcis"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permet à une appli d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"lire les paramètres et les raccourcis de la page d\'accueil"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Cette fonction est désactivée par votre administrateur"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Autoriser la rotation de l\'écran d\'accueil"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Lorsque vous faites pivoter le téléphone"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pastilles de notification"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activé"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Désactivé"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Installation de l\'appli <xliff:g id="NAME">%1$s</xliff:g> en cours, <xliff:g id="PROGRESS">%2$s</xliff:g> terminée"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Téléchargement de <xliff:g id="NAME">%1$s</xliff:g> : <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> en attente d\'installation"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"L\'appli <xliff:g id="NAME">%1$s</xliff:g> est archivée. Touchez le bouton pour télécharger et restaurer l\'appli."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"L\'appli <xliff:g id="NAME">%1$s</xliff:g> est archivée."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"télécharger et restaurer"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Mise à jour de l\'appli requise"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"L\'appli pour cette icône n\'est pas à jour. Vous pouvez soit la mettre à jour manuellement pour réactiver ce raccourci, soit retirer l\'icône."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Mettre à jour"</string>
@@ -184,12 +198,14 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Mettre en pause les applis professionnelles"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Réactiver"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrer"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Échec : <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espace privé"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Touchez pour configurer ou ouvrir"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Privé"</string>
-    <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres de l\'Espace privé"</string>
+    <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres de l\'espace privé"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privé, déverrouillé."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privé, verrouillé."</string>
     <string name="ps_container_lock_title" msgid="2640257399982364682">"Verrouiller"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 4a7c7fb..a5b9a32 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -27,18 +27,20 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Les widgets sont désactivés en mode sécurisé."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Raccourci non disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Accueil"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Définissez <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> comme application d\'accueil par défaut dans Paramètres"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Écran partagé"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Infos sur l\'appli pour %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Paramètres d\'utilisation pour %1$s"</string>
-    <string name="save_app_pair" msgid="5647523853662686243">"Enregistrer la paire d\'applis"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nouvelle fenêtre"</string>
+    <string name="save_app_pair" msgid="5647523853662686243">"Enregistrer une paire d\'applis"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applications n\'est pas prise en charge sur cet appareil"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Dépliez l\'appareil pour utiliser cette paire d\'applications"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"La paire d\'applications n\'est pas disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Appuyez de manière prolongée sur un widget pour le déplacer."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Appuyez deux fois et maintenez la pression pour déplacer widget ou utiliser actions personnalisées."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Autres options"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Afficher tous les widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d x %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largeur et %2$d de hauteur"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Prise de notes"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ajouter"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Ajoutez un widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Appuyez pour modifier les paramètres du widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifier les paramètres du widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Rechercher dans les applications"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installer"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne pas suggérer d\'appli"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Épingler la prédiction"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bulle"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installer des raccourcis"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permettre à une application d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"Lire les paramètres et les raccourcis de la page d\'accueil"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Désactivé par votre administrateur"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Autoriser la rotation de l\'écran d\'accueil"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Lorsque vous faites pivoter le téléphone"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pastilles de notification"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activées"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Désactivées"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Installation de <xliff:g id="NAME">%1$s</xliff:g>… (<xliff:g id="PROGRESS">%2$s</xliff:g> terminés)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> en cours de téléchargement, <xliff:g id="PROGRESS">%2$s</xliff:g> effectué(s)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> en attente d\'installation"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"L\'application <xliff:g id="NAME">%1$s</xliff:g> est archivée. Appuyez pour la télécharger et la restaurer."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"L\'application <xliff:g id="NAME">%1$s</xliff:g> est archivée."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"télécharger et restaurer"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Mise à jour de l\'appli requise"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"L\'appli correspondant à cette icône n\'est pas mise à jour. Vous pouvez la mettre à jour manuellement pour réactiver le raccourci ou supprimer l\'icône."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Modifier"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Mettre en pause les applis professionnelles"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Réactiver"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtre"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Échec : <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espace privé"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 0430a9a..ca2861a 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Os widgets están desactivados no modo seguro"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"O atallo non está dispoñible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Inicio"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Define <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como aplicación de inicio predeterminada en Configuración"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Información da aplicación para %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configuración de uso para %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Ventá nova"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Gardar parella de apps"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"O dispositivo non admite este emparellamento de aplicacións"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Non está dispoñible o emparellamento de aplicacións"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Mantén premido un widget para movelo."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toca dúas veces un widget e manteno premido para movelo ou utiliza accións personalizadas."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Máis opcións"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostrar todos os widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largo por %2$d de alto"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Toma de notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Engadir"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Engadir o widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toca para cambiar a configuración do widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Cambiar configuración do widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Buscar aplicacións"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalar"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Non suxerir app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fixar predición"</string>
+    <string name="bubble" msgid="3072951361014076670">"Burbulla"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalar atallos"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permite a unha aplicación engadir atallos sen intervención do usuario."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ler a configuración e os atallos da pantalla de inicio"</string>
@@ -117,10 +126,14 @@
     <string name="app_pair_name_format" msgid="8134106404716224054">"Emparellamento de aplicacións: <xliff:g id="APP1">%1$s</xliff:g> e <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Estilo e fondo de pantalla"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editar pantalla de inicio"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"Axustes de Inicio"</string>
+    <string name="settings_button_text" msgid="8873672322605444408">"Configuración da pantalla de inicio"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Función desactivada polo administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir xirar a pantalla de inicio"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Ao xirar o teléfono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puntos de notificacións"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Opción activada"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivados"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalando <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> completado"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Descargando <xliff:g id="NAME">%1$s</xliff:g> (<xliff:g id="PROGRESS">%2$s</xliff:g> completado)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Esperando para instalar <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> está no arquivo. Toca para descargar e restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> está arquivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descargar e restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"É necesario actualizar a aplicación"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"A aplicación á que corresponde esta icona non está actualizada. Podes actualizala manualmente para activar de novo este atallo, ou ben quitar a icona."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizar"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entendido"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pór en pausa aplicacións do traballo"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Volver activar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtra"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Erro: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espazo privado"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 3c248db..59e3134 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"સુરક્ષિત મોડમાં વિજેટ્સ અક્ષમ કર્યા"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"શૉર્ટકટ ઉપલબ્ધ નથી"</string>
     <string name="home_screen" msgid="5629429142036709174">"હોમ સ્ક્રીન"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"સેટિંગમાં <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>ને ડિફૉલ્ટ હોમ ઍપ તરીકે સેટ કરો"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"સ્ક્રીનને વિભાજિત કરો"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s માટે ઍપ માહિતી"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sના વપરાશ સંબંધિત સેટિંગ"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"નવી વિન્ડો"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ઍપની જોડી સાચવો"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"આ ડિવાઇસ પર, આ ઍપની જોડીને સપોર્ટ આપવામાં આવતો નથી"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ઍપની જોડી ઉપલબ્ધ નથી"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"વિજેટ ખસેડવા ટચ કરીને થોડી વાર દબાવી રાખો."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"વિજેટ ખસેડવા બે વાર ટૅપ કરીને દબાવી રાખો અથવા કસ્ટમ ક્રિયાઓનો ઉપયોગ કરો."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"વધુ વિકલ્પો"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"બધા વિજેટ બતાવો"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d પહોળાઈ X %2$d ઊંચાઈ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> વિજેટ"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"નોંધ લેવી"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ઉમેરો"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> વિજેટ ઉમેરો"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"વિજેટના સેટિંગ બદલવા માટે ટૅપ કરો"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"વિજેટના સેટિંગ બદલો"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ઍપ શોધો"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ઇન્સ્ટૉલ કરો"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ઍપ સૂચવશો નહીં"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"પૂર્વાનુમાનને પિન કરો"</string>
+    <string name="bubble" msgid="3072951361014076670">"બબલ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"શૉર્ટકટ ઇન્સ્ટૉલ કરો"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"એપ્લિકેશનને વપરાશકર્તા હસ્તક્ષેપ વગર શોર્ટકટ્સ ઉમેરવાની મંજૂરી આપે છે."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"હોમ સેટિંગ અને શૉર્ટકટ વાંચો"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"તમારા વ્યવસ્થાપક દ્વારા અક્ષમ કરેલ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"હોમ સ્ક્રીનને ફેરવવાની મંજૂરી આપો"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"જ્યારે ફોન ફેરવવામાં આવે ત્યારે"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"નોટિફિકેશન માટેના ચિહ્નો"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ચાલુ છે"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"બંધ છે"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ઇન્સ્ટૉલ કરી રહ્યાં છીએ, <xliff:g id="PROGRESS">%2$s</xliff:g> પૂર્ણ થયું"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ડાઉનલોડ કરી રહ્યાં છે, <xliff:g id="PROGRESS">%2$s</xliff:g> પૂર્ણ"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g>, ઇન્સ્ટૉલ થવાની રાહ જોઈ રહ્યું છે"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>ને આર્કાઇવ કર્યું છે. ડાઉનલોડ અને રિસ્ટોર કરવા માટે ટૅપ કરો."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g>ને આર્કાઇવ કર્યું છે."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ડાઉનલોડ અને રિસ્ટોર કરો"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ઍપને અપડેટ કરવી જરૂરી છે"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"આ આઇકન માટે ઍપ અપડેટ કરવામાં આવી નથી. તમે આ શૉર્ટકટ ફરી ચાલુ કરવા અથવા આઇકન કાઢી નાખવા માટે ઍપને મેન્યુઅલી અપડેટ કરી શકો છો."</string>
     <string name="dialog_update" msgid="2178028071796141234">"અપડેટ કરો"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"સમજાઈ ગયું"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ઑફિસની ઍપ થોભાવો"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ફરી ચાલુ કરો"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ફિલ્ટર કરો"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"નિષ્ફળ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ખાનગી સ્પેસ"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 50d4762..c26cafb 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"विजेट सुरक्षित मोड में अक्षम हैं"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"शॉर्टकट उपलब्ध नहीं है"</string>
     <string name="home_screen" msgid="5629429142036709174">"होम स्क्रीन"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"सेटिंग में जाकर, <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> को डिफ़ॉल्ट होम ऐप्लिकेशन के तौर पर सेट करें"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"स्प्लिट स्क्रीन"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s के लिए ऐप्लिकेशन की जानकारी"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s के लिए खर्च की सेटिंग"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"नई विंडो"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ऐप पेयर सेव करें"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"साथ में इस्तेमाल किए जा सकने वाले ये ऐप्लिकेशन, इस डिवाइस पर काम नहीं कर सकते"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"साथ में इस्तेमाल किए जा सकने वाले ऐप्लिकेशन की सुविधा उपलब्ध नहीं है"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"किसी विजेट को एक से दूसरी जगह ले जाने के लिए, उसे दबाकर रखें."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"किसी विजेट को एक से दूसरी जगह ले जाने के लिए, उस पर दो बार टैप करके दबाकर रखें या पसंद के मुताबिक कार्रवाइयां इस्तेमाल करें."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ज़्यादा विकल्प"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"सभी विजेट दिखाएं"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d चौड़ाई गुणा %2$d ऊंचाई"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"नोट बनाने से जुड़े विजेट"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"जोड़ें"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट जोड़ें"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेट की सेटिंग में बदलाव करने के लिए टैप करें"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेट की सेटिंग में बदलाव करें"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ऐप्लिकेशन खोजें"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"इंस्‍टॉल करें"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ऐप्लिकेशन का सुझाव न दें"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"सुझाए गए ऐप पिन करें"</string>
+    <string name="bubble" msgid="3072951361014076670">"बबल"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"शॉर्टकट इंस्‍टॉल करें"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ऐप को उपयोगकर्ता के हस्‍तक्षेप के बिना शॉर्टकट जोड़ने देती है."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"होम स्क्रीन की सेटिंग और शॉर्टकट पढ़ने की अनुमति"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"आपके एडमिन ने बंद किया हुआ है"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"होम स्क्रीन घुमाने की अनुमति दें"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"फ़ोन घुुमाए जाने पर"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"सूचनाएं बताने वाला डॉट"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"चालू है"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"चालू"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> इंस्टॉल किया जा रहा है, <xliff:g id="PROGRESS">%2$s</xliff:g> पूरा हो गया"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> डाउनलोड हो रहा है, <xliff:g id="PROGRESS">%2$s</xliff:g> पूरी हुई"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> के इंस्टॉल होने की प्रतीक्षा की जा रही है"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> को संग्रहित किया गया. ऐप्लिकेशन को वापस लाने और डाउनलोड करने के लिए टैप करें."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> को संग्रहित किया गया."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"डाउनलोड करें और वापस लाएं"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ऐप्लिकेशन को अपडेट करना ज़रूरी है"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"इस आइकॉन का ऐप्लिकेशन अपडेट नहीं है. इस शॉर्टकट को फिर से चालू करने या आइकॉन को हटाने के लिए, ऐप्लिकेशन को मैन्युअल रूप से अपडेट किया जा सकता है."</string>
     <string name="dialog_update" msgid="2178028071796141234">"अपडेट करें"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ठीक है"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"वर्क ऐप्लिकेशन रोकें"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"चालू करें"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"फ़िल्टर"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"पूरा नहीं हुआ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"प्राइवेट स्पेस"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 7a1fd94..4a29979 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgeti su onemogućeni u Sigurnom načinu rada"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Prečac nije dostupan"</string>
     <string name="home_screen" msgid="5629429142036709174">"Početni zaslon"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Postavite aplikaciju <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kao zadanu aplikaciju početnog zaslona u postavkama"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podijeljeni zaslon"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Postavke upotrebe za %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Spremi par aplikacija"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Taj par aplikacija nije podržan na ovom uređaju"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacija nije dostupan"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dodirnite i zadržite da biste premjestili widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvaput dodirnite i zadržite pritisak da biste premjestili widget ili upotrijebite prilagođene radnje"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Više opcija"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Prikaži sve widgete"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d širine i %2$d visine"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pisanje bilježaka"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodaj widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dodirnite da biste promijenili postavke widgeta"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Promijenite postavke widgeta"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pretraži aplikacije"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instaliraj"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne predlaži aplikaciju"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Prikvači predviđenu apl."</string>
+    <string name="bubble" msgid="3072951361014076670">"Oblačić"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instaliranje prečaca"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Aplikaciji omogućuje dodavanje prečaca bez intervencije korisnika."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"čitati postavke i prečace početnog zaslona"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Onemogućio administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dopusti zakretanje početnog zaslona"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kada se telefon zakrene"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Točke obavijesti"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Uključeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Isključeno"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instaliranje aplikacije <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> dovršeno"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Preuzimanje aplikacije <xliff:g id="NAME">%1$s</xliff:g>, dovršeno <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Čekanje na instaliranje aplikacije <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana. Dodirnite da biste je preuzeli i vratili."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"preuzmi i vrati"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Aplikacija se treba ažurirati"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija ove ikone nije ažurirana. Možete ručno ažurirati da biste ponovo omogućili ovaj prečac ili uklonite ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ažuriraj"</string>
@@ -184,10 +198,12 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Shvaćam"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pauziraj poslovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ponovno pokreni"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrirajte"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nije uspjelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privatni prostor"</string>
-    <string name="private_space_secondary_label" msgid="9203933341714508907">"Dodirnite da biste postavili ili otvorili"</string>
+    <string name="private_space_secondary_label" msgid="9203933341714508907">"Dodirnite za postavljanje ili otvaranje"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Privatno"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Postavke privatnog prostora"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privatno, otključano."</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 3b556da..aa1167d 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"A modulok ki vannak kapcsolva Csökkentett módban"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"A gyorsparancs nem áll rendelkezésre"</string>
     <string name="home_screen" msgid="5629429142036709174">"Kezdőképernyő"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"A(z) <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> appot a Beállításokban adhatja meg alapértelmezett kezdőalkalmazásként."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Osztott képernyő"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Alkalmazásinformáció a következőhöz: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"A(z) %1$s használati beállításai"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Új ablak"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Alkalmazáspár mentése"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ezt az alkalmazáspárt nem támogatja az eszköz"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Az alkalmazáspár nem áll rendelkezésre"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tartsa lenyomva a modult az áthelyezéshez."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Modul áthelyezéséhez koppintson duplán, tartsa nyomva az ujját, vagy használjon egyéni műveleteket."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"További lehetőségek"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Minden modul mutatása"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d széles és %2$d magas"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> modul"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Jegyzetelés"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Hozzáadás"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> modul hozzáadása"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ide koppintva módosíthatja a modulbeállításokat"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"A modulbeállítások módosítása"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Alkalmazások keresése"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Telepítés"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne javasoljon appot"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Várható kitűzése"</string>
+    <string name="bubble" msgid="3072951361014076670">"Buborék"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"parancsikonok telepítése"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Lehetővé teszi egy alkalmazás számára, hogy felhasználói beavatkozás nélkül adjon hozzá parancsikonokat."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"kezdőképernyő beállításainak és parancsikonjainak olvasása"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"A rendszergazda letiltotta"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"A kezdőképernyő elforgatásának engedélyezése"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"A telefon elforgatásakor"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Értesítési pöttyök"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Be"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Ki"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Folyamatban van a(z) <xliff:g id="NAME">%1$s</xliff:g> telepítése, <xliff:g id="PROGRESS">%2$s</xliff:g> kész"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"A(z) <xliff:g id="NAME">%1$s</xliff:g> letöltése, <xliff:g id="PROGRESS">%2$s</xliff:g> kész"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"A(z) <xliff:g id="NAME">%1$s</xliff:g> telepítésre vár"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> archiválva. Koppintson a letöltéshez és a visszaállításhoz."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> archiválva."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"letöltés és visszaállítás"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Alkalmazásfrissítés szükséges"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Az ikonhoz tartozó alkalmazás nincs frissítve. A parancsikon újbóli engedélyezéséhez frissítse az alkalmazást, vagy távolítsa ez az ikont."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Frissítés"</string>
@@ -184,11 +198,13 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Értem"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Munkahelyi alkalmazások szüneteltetése"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Folytatás"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Szűrő"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Sikertelen: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privát terület"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Koppintson a beállításhoz vagy a megnyitáshoz"</string>
-    <string name="ps_container_title" msgid="4391796149519594205">"Privát terület"</string>
+    <string name="ps_container_title" msgid="4391796149519594205">"Privát"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Privát terület beállításai"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privát, feloldott."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privát, zárolt."</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 491ba05..692984e 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Վիջեթներն անջատված են անվտանգ ռեժիմում"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Դյուրանցումն անհասանելի է"</string>
     <string name="home_screen" msgid="5629429142036709174">"Հիմնական էկրան"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Սահմանել <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> գործարկիչը որպես մեկնարկի կանխադրված հավելված Կարգավորումներում"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Տրոհել էկրանը"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Տեղեկություններ %1$s հավելվածի մասին"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Օգտագործման կարգավորումներ (%1$s)"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Նոր պատուհան"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Պահել հավելվ. զույգը"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Հավելվածների զույգը չի աջակցվում այս սարքում"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Հավելվածների զույգը հասանելի չէ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Հպեք և պահեք՝ վիջեթ տեղափոխելու համար։"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Կրկնակի հպեք և պահեք՝ վիջեթ տեղափոխելու համար, կամ օգտվեք հատուկ գործողություններից։"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Այլ ընտրանքներ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Ցույց տալ բոլոր վիջեթները"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Լայնությունը՝ %1$d, բարձրությունը՝ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> վիջեթ"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Նշումների ստեղծում"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ավելացնել"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Ավելացնել <xliff:g id="WIDGET_NAME">%1$s</xliff:g> վիջեթը"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Հպեք՝ վիջեթի կարգավորումները փոփոխելու համար"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Փոխել վիջեթի կարգավորումները"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Որոնել հավելվածներ"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Տեղադրել"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Չառաջարկել"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Ամրացնել առաջարկվող հավելվածը"</string>
+    <string name="bubble" msgid="3072951361014076670">"Ամպիկ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Դյուրանցումների տեղադրում"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Հավելվածին թույլ է տալիս ավելացնել դյուրանցումներ՝ առանց օգտագործողի միջամտության:"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"կարդալ հիմնական էկրանի կարգավորումներն ու դյուրանցումները"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Անջատվել է ձեր ադմինիստրատորի կողմից"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Թույլ տալ հիմնական էկրանի պտտումը"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Հեռախոսը պտտելու դեպքում"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Ծանուցումների կետիկներ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Միացված է"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Անջատված է"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> հավելվածը տեղադրվում է, կատարված է <xliff:g id="PROGRESS">%2$s</xliff:g>-ը"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g>–ի ներբեռնում (<xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g>-ի տեղադրման սպասում"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> հավելվածն արխիվացված է։ Հպեք՝ ներբեռնելու և վերականգնելու համար։"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> հավելվածն արխիվացված է։"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ներբեռնել և վերականգնել"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Պահանջվում է թարմացնել հավելվածը"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Հավելվածը հնացել է։ Թարմացրեք այն ձեռքով, որպեսզի շարունակեք օգտագործել դյուրանցումը, կամ հեռացրեք հավելվածի պատկերակը։"</string>
     <string name="dialog_update" msgid="2178028071796141234">"Թարմացնել"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Եղավ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Դադարեցնել աշխատանքային հավելվածները"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Վերսկսել"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Զտեք"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Չհաջողվեց կատարել գործողությունը (<xliff:g id="WHAT">%1$s</xliff:g>)"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Անձնական տարածք"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 1335385..1870055 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widget dinonaktifkan dalam mode Aman"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Pintasan tidak tersedia"</string>
     <string name="home_screen" msgid="5629429142036709174">"Layar utama"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Jadikan <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> sebagai aplikasi layar utama default di Setelan"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Layar terpisah"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Info aplikasi untuk %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Setelan penggunaan untuk %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Jendela Baru"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Simpan pasangan aplikasi"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Pasangan aplikasi ini tidak didukung di perangkat ini"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Pasangan aplikasi tidak tersedia"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Sentuh lama untuk memindahkan widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ketuk dua kali &amp; tahan untuk memindahkan widget atau gunakan tindakan khusus."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Opsi lainnya"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Tampilkan semua widget"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"lebar %1$d x tinggi %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pembuatan catatan"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Tambahkan"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Tambahkan widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ketuk untuk mengubah setelan widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Ubah setelan widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Telusuri aplikasi"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instal"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Jangan sarankan apl"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Pin Prediksi"</string>
+    <string name="bubble" msgid="3072951361014076670">"Balon"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"memasang pintasan"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Mengizinkan aplikasi menambahkan pintasan tanpa campur tangan pengguna."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"membaca setelan dan pintasan layar utama"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Dinonaktifkan oleh admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Izinkan layar utama diputar"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Saat ponsel diputar"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Titik notifikasi"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktif"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Nonaktif"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> sedang diinstal, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> sedang didownload, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> menunggu dipasang"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> diarsipkan. Ketuk untuk mendownload dan memulihkan."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> diarsipkan."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download dan pulihkan"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Aplikasi perlu diupdate"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikasi untuk ikon ini belum diupdate. Anda dapat mengupdate secara manual untuk mengaktifkan kembali pintasan ini, atau hapus ikon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Oke"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Jeda aplikasi kerja"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Aktifkan lagi"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Gagal: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Ruang privasi"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 4865264..393589a 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Græjur eru óvirkar í öruggri stillingu"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Flýtileið er ekki tiltæk"</string>
     <string name="home_screen" msgid="5629429142036709174">"Heim"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Stilltu <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> sem sjálfgefið heimaforrit í stillingunum"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Skipta skjá"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Upplýsingar um forrit fyrir %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Notkunarstillingar fyrir %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nýr gluggi"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Vista forritapar"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Þetta forritapar er ekki stutt í þessu tæki"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Forritapar er ekki í boði"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Haltu fingri á græju til að færa hana."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ýttu tvisvar og haltu fingri á græju til að færa hana eða notaðu sérsniðnar aðgerðir."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Fleiri valkostir"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Sýna allar græjur"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d á breidd og %2$d á hæð"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Græjan <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Glósugerð"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Bæta við"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Bæta græjunni <xliff:g id="WIDGET_NAME">%1$s</xliff:g> við"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ýttu til að breyta græjustillingum"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Breyta græjustillingum"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Leita í forritum"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Setja upp"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ekki fá tillögu að forriti"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Festa tillögu"</string>
+    <string name="bubble" msgid="3072951361014076670">"Blaðra"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"setja upp flýtileiðir"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Leyfir forriti að bæta við flýtileiðum án íhlutunar notanda."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"lesa stillingar og flýtileiðir heimaskjás"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Gert óvirkt af kerfisstjóra"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Leyfa snúning á heimaskjá"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Þegar símanum er snúið"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Tilkynningapunktar"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Kveikt"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Slökkt"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Setur upp <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> lokið"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> í niðurhali, <xliff:g id="PROGRESS">%2$s</xliff:g> lokið"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> bíður uppsetningar"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> er í geymslu. Ýttu til að sækja og endurheimta."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> er í geymslu."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"sækja og endurheimta"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Uppfæra þarf forritið"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Forritið fyrir þetta tákn er ekki uppfært. Þú getur uppfært það handvirkt til að kveikja aftur á þessari flýtileið eða fjarlægt táknið."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Uppfæra"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ég skil"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Setja vinnuforrit í bið"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ljúka hléi"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Sía"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mistókst: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Leynirými"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 41b4b60..2a14445 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widget disabilitati in modalità provvisoria"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"La scorciatoia non è disponibile"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Imposta <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> come app iniziale predefinita nelle Impostazioni"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Schermo diviso"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informazioni sull\'app %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Impostazioni di utilizzo per %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nuova finestra"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salva coppia di app"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Questa coppia di app non è supportata su questo dispositivo"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La coppia di app non è disponibile"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tocca e tieni premuto per spostare un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Tocca due volte e tieni premuto per spostare un widget o per usare le azioni personalizzate."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Altre opzioni"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostra tutti i widget"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d di larghezza per %2$d di altezza"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Aggiunta di note"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Aggiungi"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Aggiungi widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tocca per modificare le impostazioni del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifica le impostazioni del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Cerca nelle app"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installa"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Non suggerire app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Blocca previsione"</string>
+    <string name="bubble" msgid="3072951361014076670">"Fumetto"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Aggiunta di scorciatoie"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Consente a un\'app di aggiungere scorciatoie automaticamente."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"leggere le impostazioni e le scorciatoie nella schermata Home"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disattivata dall\'amministratore"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Consenti rotazione della schermata Home"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Con il telefono ruotato"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Indicatori di notifica"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Installazione di <xliff:g id="NAME">%1$s</xliff:g>, completamento: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Download di <xliff:g id="NAME">%1$s</xliff:g> in corso, <xliff:g id="PROGRESS">%2$s</xliff:g> completato"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> in attesa di installazione"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"App <xliff:g id="NAME">%1$s</xliff:g> archiviata. Tocca per scaricare e ripristinare."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"App <xliff:g id="NAME">%1$s</xliff:g> archiviata."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download e ripristino"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"È necessario aggiornare l\'app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"L\'app relativa a questa icona non è aggiornata. Puoi eseguire manualmente l\'aggiornamento per riattivare questa scorciatoia oppure rimuovere l\'icona."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aggiorna"</string>
@@ -184,12 +198,14 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Metti in pausa le app di lavoro"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Riattiva"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtra"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Operazione non riuscita: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Spazio privato"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Tocca per configurare o aprire"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Privato"</string>
-    <string name="ps_container_settings" msgid="6059734123353320479">"Impostazioni dello Spazio privato"</string>
+    <string name="ps_container_settings" msgid="6059734123353320479">"Impostazioni dello spazio privato"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privato, sbloccato."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privato, bloccato."</string>
     <string name="ps_container_lock_title" msgid="2640257399982364682">"Blocca"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index ae71edd..724742a 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ווידג\'טים מושבתים במצב בטוח"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"קיצור הדרך אינו זמין"</string>
     <string name="home_screen" msgid="5629429142036709174">"בית"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"הגדרה של <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> כאפליקציית הבית ב\'הגדרות\'"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"מסך מפוצל"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏פרטים על האפליקציה %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏הגדרות שימוש ב-%1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"חלון חדש"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"שמירת צמד אפליקציות"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"צמד האפליקציות הזה לא נתמך במכשיר הזה"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"צמד האפליקציות לא זמין"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"להעברת ווידג\'ט למקום אחר לוחצים עליו לחיצה ארוכה."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"כדי להעביר ווידג\'ט למקום אחר או להשתמש בפעולות מותאמות אישית, יש ללחוץ פעמיים ולא להרפות."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"אפשרויות נוספות"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"הצגת כל הווידג\'טים"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏רוחב %1$d על גובה %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"ווידג\'ט <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -62,11 +64,17 @@
     <string name="no_widgets_available" msgid="4337693382501046170">"אין ווידג\'טים או קיצורי דרך"</string>
     <string name="no_search_results" msgid="3787956167293097509">"לא נמצאו ווידג\'טים או קיצורי דרך"</string>
     <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"ווידג\'טים אישיים"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"עבודה"</string>
+    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ווידג\'טים לעבודה"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"שיחות"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"כתיבת הערות"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"הוספה"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"הוספת הווידג\'ט <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"אפשר לשנות את הגדרות הווידג\'ט בהקשה"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"שינוי הגדרות הווידג\'ט"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"חיפוש אפליקציות"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"התקנה"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"בלי להציע את האפליקציה"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"הצמדת החיזוי"</string>
+    <string name="bubble" msgid="3072951361014076670">"בועה"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"התקנת קיצורי דרך"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"מאפשר לאפליקציה להוסיף קיצורי דרך ללא התערבות המשתמש."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"קריאת ההגדרות וקיצורי הדרך בדף הבית"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"הושבת על ידי מנהל המערכת שלך"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"סיבוב מסך הבית"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"כאשר מסובבים את הטלפון"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"סימני ההתראות"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"מופעל"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"כבוי"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> בתהליך התקנה, <xliff:g id="PROGRESS">%2$s</xliff:g> הושלמו"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"הורדת <xliff:g id="NAME">%1$s</xliff:g> מתבצעת, <xliff:g id="PROGRESS">%2$s</xliff:g> הושלמו"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"מחכה להתקנה של <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"אפליקציית <xliff:g id="NAME">%1$s</xliff:g> הועברה לארכיון. אפשר להקיש כדי להוריד ולשחזר אותה."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"אפליקציית <xliff:g id="NAME">%1$s</xliff:g> הועברה לארכיון."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"הורדה ושחזור"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"נדרש עדכון לאפליקציה"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"האפליקציה של הסמל הזה לא מעודכנת. אפשר לעדכן אותה ידנית כדי להפעיל מחדש את קיצור הדרך הזה, או להסיר את הסמל."</string>
     <string name="dialog_update" msgid="2178028071796141234">"עדכון"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"הבנתי"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"השהיית האפליקציות לעבודה"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ביטול ההשהיה"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"סינון"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"הפעולה נכשלה: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"מרחב פרטי"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index c64d335..29dbf26 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"セーフモードではウィジェットは無効です"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ショートカットは使用できません"</string>
     <string name="home_screen" msgid="5629429142036709174">"ホーム"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"[設定] で <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> をデフォルトのホームアプリに設定します"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割画面"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s のアプリ情報"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s の使用設定"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新しいウィンドウ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"アプリのペア設定を保存"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"このデバイスは、このアプリのペア設定に対応していません"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"アプリのペア設定は利用できません"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"長押ししてウィジェットを移動させます。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ウィジェットをダブルタップして長押ししながら移動するか、カスタム操作を使用してください。"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"その他のオプション"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"すべてのウィジェットを表示"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$dx%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"幅 %1$d、高さ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ウィジェット"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"メモ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"追加"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>ウィジェットを追加"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"タップしてウィジェットの設定を変更する"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ウィジェットの設定を変更します"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"アプリを検索"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"インストール"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"アプリを表示しない"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"アプリの候補を固定"</string>
+    <string name="bubble" msgid="3072951361014076670">"ふきだし"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ショートカットのインストール"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ユーザー操作なしでショートカットを追加することをアプリに許可します。"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ホームの設定とショートカットの読み取り"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"管理者により無効にされています"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ホーム画面の回転を許可"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"スマートフォンの向きに合わせます"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知ドット"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ON"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"OFF"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> をインストールしています: <xliff:g id="PROGRESS">%2$s</xliff:g> 完了"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g>をダウンロード中、<xliff:g id="PROGRESS">%2$s</xliff:g>完了"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g>のインストール待ち"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>はアーカイブ済みです。ダウンロードして復元するには、タップしてください。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> はアーカイブ済みです。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ダウンロードして復元"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"アプリの更新が必要"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"このアイコンのアプリは更新されていません。手動で更新して、このショートカットを再度有効にできます。また、アイコンを削除することもできます。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"仕事用アプリを一時停止"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"停止解除"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"フィルタ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"失敗: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"プライベート スペース"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index bcced9a..876ab11 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"უსაფრთხო რეჟიმში ვიჯეტი გამორთულია"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"მალსახმობი მიუწვდომელია"</string>
     <string name="home_screen" msgid="5629429142036709174">"მთავარი გვერდი"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>-ის დაყენება ნაგულისხმევ Home აპად პარამეტრებში"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ეკრანის გაყოფა"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s-ის აპის ინფო"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"გამოყენების პარამეტრები %1$s-ისთვის"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ახალი ფანჯარა"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"აპთა წყვილის შენახვა"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ამ მოწყობილობაზე აღნიშნული აპთა წყვილი არ არის მხარდაჭერილი"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"აპთა წყვილი მიუწვდომელია"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"შეხებით აირჩიეთ და გეჭიროთ ვიჯეტის გადასაადგილებლად."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ორმაგი შეხებით აირჩიეთ და გეჭიროთ ვიჯეტის გადასაადგილებლად ან მორგებული მოქმედებების გამოსაყენებლად."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"სხვა ვარიანტები"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ყველა ვიჯეტის ჩვენება"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"სიგრძე: %1$d, სიგანე: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ვიჯეტი"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ჩანიშვნა"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"დამატება"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ვიჯეტის დამატება"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"შეეხეთ ვიჯეტის პარამეტრების შესაცვლელად"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ვიჯეტის პარამეტრების შეცვლა"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"აპების ძიება"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ინსტალაცია"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"არ შემომთავაზო აპი"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ჩამაგრების პროგნოზირება"</string>
+    <string name="bubble" msgid="3072951361014076670">"ბუშტი"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"მალსახმობების დაყენება"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"აპისთვის მალსახმობების დამოუკიდებლად დამატების უფლების მიცემა."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"მთავარი ეკრანის პარამეტრებისა და მალსახმობების წაკითხვა"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"გათიშულია თქვენი ადმინისტრატორის მიერ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"მთავარი ეკრანის შეტრიალების დაშვება"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ტელეფონის შეტრიალებისას"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"შეტყობინების ნიშნულები"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ჩართულია"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"გამორთულია"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"ინსტალირდება <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> დასრულებულია"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"მიმდინარეობს <xliff:g id="NAME">%1$s</xliff:g>-ის ჩამოტვირთვა, <xliff:g id="PROGRESS">%2$s</xliff:g> დასრულდა"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ელოდება ინსტალაციას"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> დაარქივებულია. შეეხეთ გადმოსაწერად და აღსადგენად."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> დაარქივებულია."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ჩამოტვირთვა და აღდგენა"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"საჭიროა აპის განახლება"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ამ ხატულის აპი განახლებული არ არის. შეგიძლიათ, ხელით განაახლოთ ამ მალსახმობის ხელახლა გასააქტიურებლად, ან ამოშალოთ ხატულა."</string>
     <string name="dialog_update" msgid="2178028071796141234">"განახლება"</string>
@@ -184,11 +198,13 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"გასაგებია"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"სამსახურის აპების დაპაუზება"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"პაუზის გაუქმება"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ფილტრი"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ვერ მოხერხდა: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"პირადი სივრცე"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"დასაყენებლად ან გასახსნელად შეეხეთ"</string>
-    <string name="ps_container_title" msgid="4391796149519594205">"პირადი"</string>
+    <string name="ps_container_title" msgid="4391796149519594205">"კერძო"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"პირადი სივრცის პარამეტრები"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"პირადი (განბლოკილი)."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"პირადი (ჩაკეტილი)."</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 62bb983..bc1c380 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Қауіпсіз режимде виджеттер өшіріледі"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Таңбаша қолжетімді емес"</string>
     <string name="home_screen" msgid="5629429142036709174">"Негізгі экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Параметрлерден <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> қолданбасын әдепкі негізгі экран қолданбасы ретінде орнатыңыз."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Экранды бөлу"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s қолданбасы туралы ақпарат"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s пайдалану параметрлері"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Жаңа терезе"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Қолданбаларды жұптау әрекетін сақтау"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Бұл құрылғы қолданбаларды жұптау функциясын қолдамайды."</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Қолданбаларды жұптау функциясы қолжетімді емес."</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Виджетті жылжыту үшін басып тұрыңыз."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Виджетті жылжыту үшін екі рет түртіңіз де, ұстап тұрыңыз немесе арнаулы әрекеттерді пайдаланыңыз."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Басқа опциялар"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Барлық виджетті көрсету"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ені: %1$d, биіктігі: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджеті"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ескертпе жазу"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Қосу"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Виджет (<xliff:g id="WIDGET_NAME">%1$s</xliff:g>) қосу"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Виджет параметрлерін өзгерту үшін түртіңіз."</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Виджет параметрлерін өзгерту"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Қолданбаларды іздеу"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Орнату"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Қолданба ұсынбау"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Болжамды бекіту"</string>
+    <string name="bubble" msgid="3072951361014076670">"Қалқыма терезе"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"таңбаша орнату"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Қолданбаға пайдаланушының қатысуынсыз төте пернелерді қосу мүмкіндігін береді."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"негізгі экран параметрлері мен таңбашаларын оқу"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Әкімші өшірді"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Негізгі экранды бұруға рұқсат ету"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Телефон бұрылғанда"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Хабарландыру белгілері"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Қосулы"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Өшірулі"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> орнатылуда, <xliff:g id="PROGRESS">%2$s</xliff:g> аяқталды"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> жүктелуде, <xliff:g id="PROGRESS">%2$s</xliff:g> аяқталды"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> орнату күтілуде"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> мұрағатталды. Жүктеп алу және қалпына келтіру үшін түртіңіз."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> мұрағатталды."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"жүктеп алу және қалпына келтіру"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Қолданбаны жаңарту қажет"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Осы белгіше үшін қолданба жаңартылмаған. Оны қолмен жаңартып, осы таңбашаны қайта іске қоса аласыз немесе белгішені өшіріп тастаңыз."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Жаңарту"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Түсінікті"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Жұмыс қолданбаларын кідірту"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Қайта қосу"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Сүзгі"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Қате шықты: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Құпия кеңістік"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index b733699..813b5a2 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"បាន​បិទ​ធាតុ​ក្រាហ្វិក​ក្នុង​របៀប​សុវត្ថិភាព"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ផ្លូវកាត់មិនអាចប្រើបានទេ"</string>
     <string name="home_screen" msgid="5629429142036709174">"អេក្រង់ដើម"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"កំណត់ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ជាកម្មវិធីអេក្រង់ដើមលំនាំដើមនៅក្នុង \"ការកំណត់\""</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"មុខងារ​បំបែកអេក្រង់"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"ព័ត៌មានកម្មវិធី​សម្រាប់ %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"ការកំណត់ការប្រើប្រាស់សម្រាប់ %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"វិនដូ​ថ្មី"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"រក្សាទុកគូកម្មវិធី"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"មិនអាចប្រើគូកម្មវិធីនេះនៅលើឧបករណ៍នេះបានទេ"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"មិនអាចប្រើគូកម្មវិធីបានទេ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ចុចឱ្យជាប់​ដើម្បីផ្លាស់ទី​ធាតុក្រាហ្វិក​។"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ចុចពីរដង រួចសង្កត់ឱ្យជាប់ ដើម្បីផ្លាស់ទី​ធាតុក្រាហ្វិក ឬប្រើ​សកម្មភាព​តាមបំណង​។"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ជម្រើស​ច្រើនទៀត"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"បង្ហាញគ្រប់ធាតុ​ក្រាហ្វិក"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"ទទឺង %1$d គុណនឹងកម្ពស់ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"ធាតុ​ក្រាហ្វិក <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ការកត់ត្រា"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"បញ្ចូល"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"បញ្ចូលធាតុ​ក្រាហ្វិក <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ចុចដើម្បីប្ដូរការកំណត់ធាតុ​ក្រាហ្វិក"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ប្ដូរការកំណត់ធាតុ​ក្រាហ្វិក"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ស្វែងរក​កម្មវិធី"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ដំឡើង"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"កុំណែនាំកម្មវិធី"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ខ្ទាស់ការ​ព្យាករ"</string>
+    <string name="bubble" msgid="3072951361014076670">"ពពុះ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ដំឡើង​ផ្លូវកាត់"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"អនុញ្ញាត​ឲ្យ​កម្មវិធី​បន្ថែម​ផ្លូវកាត់​ ដោយ​មិន​ចាំបាច់​​អំពើ​ពី​អ្នក​ប្រើ។"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"អានការកំណត់ និងផ្លូវកាត់របស់អេក្រង់ដើម"</string>
@@ -115,12 +124,16 @@
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ថត៖ <xliff:g id="NAME">%1$s</xliff:g>, ធាតុ <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ថត៖ <xliff:g id="NAME">%1$s</xliff:g>, ធាតុ <xliff:g id="SIZE">%2$d</xliff:g> ឬច្រើនជាងនេះ"</string>
     <string name="app_pair_name_format" msgid="8134106404716224054">"គូកម្មវិធី៖ <xliff:g id="APP1">%1$s</xliff:g> និង <xliff:g id="APP2">%2$s</xliff:g>"</string>
-    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ផ្ទាំងរូបភាព និងរចនាប័ទ្ម"</string>
+    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ផ្ទាំងរូបភាព និងរចនាបថ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"កែអេក្រង់ដើម"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ការកំណត់​ទំព័រដើម"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"បានបិទដំណើរការដោយអ្នកគ្រប់គ្រងរបស់អ្នក"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"អនុញ្ញាតការបងិ្វលអេក្រង់ដើម"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"នៅពេលដែលបង្វិលទូរសព្ទ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ស្លាកជូនដំណឹង"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"បើក"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"បិទ"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"កំពុង​ដំឡើង <xliff:g id="NAME">%1$s</xliff:g>, បាន​បញ្ចប់ <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"កំពុងដោនឡូត <xliff:g id="NAME">%1$s</xliff:g> បានបញ្ចប់ <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> កំពុងរង់ចាំការដំឡើង"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ត្រូវបានទុក​ក្នុង​បណ្ណសារ។ សូមចុចដើម្បីទាញយក និងស្ដារ។"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ត្រូវបានទុក​ក្នុង​បណ្ណសារ។"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ទាញយក និងស្ដារ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"តម្រូវឱ្យមាន​កំណែកម្មវិធីថ្មី"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"កម្មវិធីសម្រាប់​រូបតំណាងនេះ​មិនត្រូវបានដំឡើងកំណែ​ទេ។ អ្នកអាច​ដំឡើងកំណែ​ដោយផ្ទាល់ ដើម្បីបើក​ផ្លូវកាត់នេះឡើងវិញ ឬលុបរូបតំណាងនេះ។"</string>
     <string name="dialog_update" msgid="2178028071796141234">"ដំឡើងកំណែ"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"យល់ហើយ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ផ្អាក​កម្មវិធី​ការងារ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ឈប់ផ្អាក"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"តម្រង"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"បានបរាជ័យ៖ <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"បន្ទប់​ឯកជន"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 16b39f8..b4bc3a1 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -21,17 +21,17 @@
     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>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ಶಾರ್ಟ್‌ಕಟ್ ಲಭ್ಯವಿಲ್ಲ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ಹೋಮ್"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ಅನ್ನು ಡೀಫಾಲ್ಟ್ ಹೋಮ್ ಆ್ಯಪ್ ಆಗಿ ಸೆಟ್‌ ಮಾಡಿ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ಗಾಗಿ ಆ್ಯಪ್ ಮಾಹಿತಿ"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ಗೆ ಸಂಬಂಧಿಸಿದ ಬಳಕೆಯ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ಹೊಸ ವಿಂಡೋ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ಆ್ಯಪ್ ಪೇರ್ ಸೇವ್ ಮಾಡಿ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ಈ ಆ್ಯಪ್ ಜೋಡಿಯು ಈ ಸಾಧನದಲ್ಲಿ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ಆ್ಯಪ್ ಜೋಡಿ ಲಭ್ಯವಿಲ್ಲ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ವಿಜೆಟ್ ಸರಿಸಲು ಸ್ಪರ್ಶಿಸಿ ಮತ್ತು ಹಿಡಿದುಕೊಳ್ಳಿ."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ವಿಜೆಟ್ ಸರಿಸಲು ಅಥವಾ ಕಸ್ಟಮ್ ಕ್ರಿಯೆಗಳನ್ನು ಬಳಸಲು ಡಬಲ್-ಟ್ಯಾಪ್ ಮಾಡಿ ಮತ್ತು ಹಿಡಿದುಕೊಳ್ಳಿ."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ಎಲ್ಲಾ ವಿಜೆಟ್‌ ತೋರಿಸಿ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ಅಗಲ ಮತ್ತು %2$d ಎತ್ತರ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ವಿಜೆಟ್"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ಟಿಪ್ಪಣಿ ತೆಗೆದುಕೊಳ್ಳುವುದು"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ಸೇರಿಸಿ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ವಿಜೆಟ್ ಸೇರಿಸಿ"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಿ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ಆ್ಯಪ್‍ಗಳನ್ನು ಹುಡುಕಿ"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ಸ್ಥಾಪಿಸಿ"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ಆ್ಯಪ್ ಅನ್ನು ಸೂಚಿಸಬೇಡಿ"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ಮುನ್ನೋಟ ಪಿನ್ ಮಾಡಿ"</string>
+    <string name="bubble" msgid="3072951361014076670">"ಬಬಲ್"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಸ್ಥಾಪಿಸಿ"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ಬಳಕೆದಾರರ ಹಸ್ತಕ್ಷೇಪವಿಲ್ಲದೆ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಸೇರಿಸಲು ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ಹೋಮ್ ಸ್ಕ್ರೀನ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಓದಿ"</string>
@@ -100,7 +109,7 @@
     <string name="gadget_error_text" msgid="740356548025791839">"ವಿಜೆಟ್ ಅನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
     <string name="gadget_setup_text" msgid="8348374825537681407">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
     <string name="gadget_complete_setup_text" msgid="309040266978007925">"ಸೆಟಪ್ ಪೂರ್ಣಗೊಳಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
-    <string name="uninstall_system_app_text" msgid="4172046090762920660">"ಇದೊಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಗಿದೆ ಮತ್ತು ಅಸ್ಥಾಪಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
+    <string name="uninstall_system_app_text" msgid="4172046090762920660">"ಇದೊಂದು ಆ್ಯಪ್‌ ಆಗಿದೆ ಮತ್ತು ಅಸ್ಥಾಪಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
     <string name="folder_hint_text" msgid="5174843001373488816">"ಹೆಸರನ್ನು ಎಡಿಟ್ ಮಾಡಿ"</string>
     <string name="disabled_app_label" msgid="6673129024321402780">"<xliff:g id="APP_NAME">%1$s</xliff:g> ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
     <string name="dotted_app_label" msgid="1865617679843363410">"{count,plural, =1{{app_name} ಆ್ಯಪ್ # ಅಧಿಸೂಚನೆಯನ್ನು ಹೊಂದಿದೆ}one{{app_name} ಆ್ಯಪ್ # ಅಧಿಸೂಚನೆಗಳನ್ನು ಹೊಂದಿದೆ}other{{app_name} ಆ್ಯಪ್ # ಅಧಿಸೂಚನೆಗಳನ್ನು ಹೊಂದಿದೆ}}"</string>
@@ -117,10 +126,14 @@
     <string name="app_pair_name_format" msgid="8134106404716224054">"ಆ್ಯಪ್ ಜೋಡಿ: <xliff:g id="APP1">%1$s</xliff:g> ಮತ್ತು <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ವಾಲ್‌ಪೇಪರ್ ಮತ್ತು ಶೈಲಿ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ಹೋಮ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಎಡಿಟ್ ಮಾಡಿ"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"ಮುಖಪುಟ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
+    <string name="settings_button_text" msgid="8873672322605444408">"ಹೋಮ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದ್ದಾರೆ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ಹೋಮ್ ಸ್ಕ್ರೀನ್ ತಿರುಗುವಿಕೆಯನ್ನು ಅನುಮತಿಸಿ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ಫೋನ್‌ ತಿರುಗಿಸಿದಾಗ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ನೋಟಿಫಿಕೇಶನ್ ಡಾಟ್‌ಗಳು"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ಆನ್ ಆಗಿದೆ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ಆಫ್ ಆಗಿದೆ"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗುತ್ತಿದೆ, <xliff:g id="PROGRESS">%2$s</xliff:g> ಪೂರ್ಣಗೊಂಡಿದೆ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ಡೌನ್‌ಲೋಡ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ, <xliff:g id="PROGRESS">%2$s</xliff:g> ಪೂರ್ಣಗೊಂಡಿದೆ"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ಸ್ಥಾಪಿಸಲು ಕಾಯಲಾಗುತ್ತಿದೆ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ಅನ್ನು ಆರ್ಕೈವ್ ಮಾಡಲಾಗಿದೆ. ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಮತ್ತು ಮರುಸ್ಥಾಪಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ಅನ್ನು ಆರ್ಕೈವ್ ಮಾಡಲಾಗಿದೆ."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ ಮತ್ತು ಮರುಸ್ಥಾಪಿಸಿ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ಆ್ಯಪ್ ಅಪ್‌ಡೇಟ್ ಅಗತ್ಯವಿದೆ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ಈ ಐಕಾನ್‌ಗಾಗಿ ಆ್ಯಪ್ ಅನ್ನು ಅಪ್‌ಡೇಟ್ ಮಾಡಲಾಗಿಲ್ಲ. ಈ ಶಾರ್ಟ್‌ಕಟ್ ಅನ್ನು ಮರು-ಸಕ್ರಿಯಗೊಳಿಸಲು ನೀವು ಹಸ್ತಚಾಲಿತವಾಗಿ ಅಪ್‌ಡೇಟ್ ಮಾಡಬಹುದು ಅಥವಾ ಐಕಾನ್ ಅನ್ನು ತೆಗೆದುಹಾಕಬಹುದು."</string>
     <string name="dialog_update" msgid="2178028071796141234">"ಅಪ್‌ಡೇಟ್ ಮಾಡಿ"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ಅರ್ಥವಾಯಿತು"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳನ್ನು ವಿರಾಮಗೊಳಿಸಿ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ವಿರಾಮವನ್ನು ರದ್ದುಗೊಳಿಸಿ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ಫಿಲ್ಟರ್‌"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ವಿಫಲವಾಗಿದೆ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ಖಾಸಗಿ ಸ್ಪೇಸ್"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 1d5c58f..4baba23 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"안전 모드에서 위젯 사용 중지됨"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"바로가기를 사용할 수 없음"</string>
     <string name="home_screen" msgid="5629429142036709174">"홈"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"설정에서 <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> 런처를 기본 홈 앱으로 설정하세요."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"화면 분할"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s 앱 정보"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s의 사용량 설정"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"새 창"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"앱 페어링 저장"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"이 앱 페어링은 이 기기에서 지원되지 않습니다"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"앱 페어링을 사용할 수 없습니다."</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"길게 터치하여 위젯을 이동하세요."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"두 번 탭한 다음 길게 터치하여 위젯을 이동하거나 맞춤 작업을 사용하세요."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"옵션 더보기"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"모든 위젯 표시"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d×%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"너비 %1$d, 높이 %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"위젯 <xliff:g id="WIDGET_NAME">%1$s</xliff:g>개"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"메모"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"추가"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> 위젯 추가"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"탭하여 위젯 설정 변경"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"위젯 설정 변경"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"앱 검색"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"설치"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"앱 제안 받지 않음"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"예상 앱 고정"</string>
+    <string name="bubble" msgid="3072951361014076670">"풍선"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"바로가기 설치"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"앱이 사용자의 작업 없이 바로가기를 추가할 수 있도록 합니다."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"홈 설정 및 바로가기 읽기"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"관리자가 사용 중지함"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"홈 화면 회전 허용"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"휴대전화 회전 시"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"알림 표시 점"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"사용"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"사용 안함"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> 설치 중, <xliff:g id="PROGRESS">%2$s</xliff:g> 완료"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> 다운로드 중, <xliff:g id="PROGRESS">%2$s</xliff:g> 완료"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> 설치 대기 중"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> 앱이 보관처리되었습니다. 탭하여 다운로드하고 복원하세요"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> 앱이 보관처리되었습니다"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"다운로드 및 복원"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"앱 업데이트 필요"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"바로가기 아이콘의 앱이 업데이트되지 않았습니다. 직접 업데이트하여 앱 바로가기를 다시 사용할 수 있도록 하거나 아이콘을 삭제하세요."</string>
     <string name="dialog_update" msgid="2178028071796141234">"업데이트"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"확인"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"직장 앱 일시중지"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"일시중지 해제"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"필터"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"실패: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"비공개 스페이스"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 6f01db3..83b0fa8 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Виджеттер Коопсуз режимде өчүрүлгөн"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Кыска жол жок"</string>
     <string name="home_screen" msgid="5629429142036709174">"Башкы экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Параметрлерден <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> жүргүзгүчүн демейки башкы бет колдонмосу катары коюу"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Экранды бөлүү"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s колдонмосу жөнүндө маалымат"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s колдонмосун пайдалануу параметрлери"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Жаңы терезе"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Колдонмолорду сактап коюу"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Бул эки колдонмону бул түзмөктө бир маалда пайдаланууга болбойт"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Эки колдонмону бир маалда пайдаланууга болбойт"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Виджетти кое бербей басып туруп жылдырыңыз."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Виджетти жылдыруу үчүн эки жолу таптап, кармап туруңуз же ыңгайлаштырылган аракеттерди колдонуңуз."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Дагы параметрлер"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Виджеттин баарын көрсөтүү"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Туурасы: %1$d, бийиктиги: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджети"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Эскертме жазуу"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Кошуу"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджетин кошуу"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Виджеттин параметрлерин өзгөртүү үчүн таптап коюңуз"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Виджеттин параметрлерин өзгөртүү"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Колдонмолорду издөө"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Орнотуу"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Cунушталбасын"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Божомолдонгон колдонмону кадап коюу"</string>
+    <string name="bubble" msgid="3072951361014076670">"Көбүкчө"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"тез чакырмаларды орнотуу"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Колдонмого колдонуучуга кайрылбастан тез чакырма кошууга уруксат берет."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"үйдүн параметрлерин жана ыкчам баскычтарын окуу"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Администраторуңуз өчүрүп койгон"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Башкы экранды бурууга уруксат берүү"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Телефон бурулганда"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Билдирмелер белгилери"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Күйүк"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Өчүк"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> орнотулууда, <xliff:g id="PROGRESS">%2$s</xliff:g> аткарылды"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> жүктөлүп алынууда, <xliff:g id="PROGRESS">%2$s</xliff:g> аяктады"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> орнотулушу күтүлүүдө"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> архивделди. Жүктөп алуу жана калыбына келтирүү үчүн таптаңыз."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> архивделди."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"жүктөп алуу жана калыбына келтирүү"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Колдонмону жаңыртыңыз"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Бул сүрөтчөнүн колдонмосу жаңыртылган эмес. Ыкчам баскычты кайра иштетүү үчүн аны кол менен жаңыртып же сүрөтчөнү өчүрүп койсоңуз болот."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Жаңыртуу"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Түшүндүм"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Жумуш колдонмолорун тындыруу"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Улантуу"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Чыпкалоо"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Аткарылган жок: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Жеке мейкиндик"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 7ec103b..767c574 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"​ວິດ​ເຈັດ​ຖືກ​ປິດ​ໃນ Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ບໍ່ສາມາດໃຊ້ທາງລັດໄດ້"</string>
     <string name="home_screen" msgid="5629429142036709174">"ໂຮມສະກຣີນ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ຕັ້ງ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ເປັນແອັບໂຮມເລີ່ມຕົ້ນໃນການຕັ້ງຄ່າ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ແບ່ງໜ້າຈໍ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"ຂໍ້ມູນແອັບສຳລັບ %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"ການຕັ້ງຄ່າການນຳໃຊ້ສຳລັບ %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ໜ້າຈໍໃໝ່"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ບັນທຶກຈັບຄູ່ແອັບ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ການຈັບຄູ່ແອັບນີ້ບໍ່ຮອງຮັບຢູ່ອຸປະກອນນີ້"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ການຈັບຄູ່ແອັບບໍ່ມີໃຫ້"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ແຕະຄ້າງໄວ້ເພື່ອຍ້າຍວິດເຈັດ."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ແຕະສອງເທື່ອຄ້າງໄວ້ເພື່ອຍ້າຍວິດເຈັດ ຫຼື ໃຊ້ຄຳສັ່ງກຳນົດເອງ."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ຕົວເລືອກເພີ່ມເຕີມ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ສະແດງວິດເຈັດທັງໝົດ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"ກວ້າງ %1$d ຄູນສູງ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"ວິດເຈັດ <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ການຈົດບັນທຶກ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ເພີ່ມ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"ເພີ່ມວິດເຈັດ <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ແຕະເພື່ອປ່ຽນການຕັ້ງຄ່າວິດເຈັດ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ປ່ຽນການຕັ້ງຄ່າວິດເຈັດ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ຊອກຫາແອັບ"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ຕິດຕັ້ງ"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ຢ່າແນະນຳແອັບ"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ປັກໝຸດການຄາດເດົາ"</string>
+    <string name="bubble" msgid="3072951361014076670">"ຟອງ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ຕິດຕັ້ງທາງລັດ"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ອະນຸຍາດໃຫ້ແອັບຯ ເພີ່ມທາງລັດໂດຍບໍ່ຕ້ອງຮັບການຢືນຢັນຈາກຜູ່ໃຊ້."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ອ່ານການຕັ້ງຄ່າໜ້າຫຼັກ ແລະ ທາງລັດ"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ຖືກປິດການນຳໃຊ້ໂດຍຜູ້ເບິ່ງແຍງລະບົບຂອງທ່ານ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ອະນຸຍາດໃຫ້ໝຸນໜ້າຈໍຢູ່ໂຮມສະກຣີນໄດ້"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ເມື່ອໝຸນໂທລະສັບ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ຈຸດການແຈ້ງເຕືອນ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ເປີດ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ປິດ"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"ກຳລັງຕິດຕັ້ງ <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> ສຳເລັດແລ້ວ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ກຳ​ລັງ​ດາວ​ໂຫຼດ, <xliff:g id="PROGRESS">%2$s</xliff:g> ສຳ​ເລັດ"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ກຳ​ລັງ​ລໍ​ຖ້າ​ຕິດ​ຕັ້ງ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ຖືກເກັບໄວ້ໃນແຟ້ມ. ແຕະເພື່ອດາວໂຫຼດ ແລະ ກູ້ຄືນ."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ຖືກເກັບໄວ້ໃນແຟ້ມ."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ດາວໂຫຼດ ແລະ ກູ້ຄືນ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ຈຳເປັນຕ້ອງອັບເດດແອັບ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ບໍ່ໄດ້ອັບເດດແອັບສຳລັບໄອຄອນນີ້. ທ່ານສາມາດອັບເດດເອງໄດ້ເພື່ອເປີດການນຳໃຊ້ທາງລັດນີ້ຄືນໃໝ່ ຫຼື ລຶບໄອຄອນດັ່ງກ່າວອອກ."</string>
     <string name="dialog_update" msgid="2178028071796141234">"ອັບເດດ"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ເຂົ້າໃຈແລ້ວ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ຢຸດແອັບບ່ອນເຮັດວຽກຊົ່ວຄາວ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ຍົກເລີກການຢຸດຊົ່ວຄາວ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ກັ່ນຕອງ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ບໍ່ສຳເລັດ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ພື້ນທີ່ສ່ວນຕົວ"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 1112474..08b603d 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Valdikliai išjungti Saugiame režime"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Sparčiojo klavišo negalima naudoti"</string>
     <string name="home_screen" msgid="5629429142036709174">"Pagrindinis"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Nustatykite „<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>“ kaip numatytąją pagrindinę programą skiltyje „Nustatymai“"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Išskaidyto ekrano režimas"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Programos „%1$s“ informacija"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"„%1$s“ naudojimo nustatymai"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Naujas langas"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Išsaugoti programų porą"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ši programų pora šiame įrenginyje nepalaikoma"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Programų pora nepasiekiama"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dukart pal. ir palaik., kad perkeltumėte valdiklį."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dukart palieskite ir palaikykite, kad perkeltumėte valdiklį ar naudotumėte tinkintus veiksmus."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Daugiau parinkčių"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Rodyti visus valdiklius"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d plotis ir %2$d aukštis"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> valdiklis"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Užrašų kūrimas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Pridėti"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Pridėti valdiklį: <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Palieskite, kad pakeistumėte valdiklio nustatymus"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Pakeisti valdiklio nustatymus"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Paieškos programos"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Įdiegti"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Nesiūlyti programos"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Prisegti numatymą"</string>
+    <string name="bubble" msgid="3072951361014076670">"Debesėlis"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"įdiegti sparčiuosius klavišus"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Programai leidžiama pridėti sparčiuosius klavišus be naudotojo įsikišimo."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"skaityti pagrindinio ekrano nustatymus ir sparčiuosius klavišus"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Išjungė administratorius"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Leisti pasukti pagrindinį ekraną"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kai telefonas pasukamas"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pranešimų taškai"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Įjungta"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Išjungta"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Įdiegiama: „<xliff:g id="NAME">%1$s</xliff:g>“; baigta: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Atsisiunčiama programa „<xliff:g id="NAME">%1$s</xliff:g>“, <xliff:g id="PROGRESS">%2$s</xliff:g> baigta"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Laukiama, kol bus įdiegta programa „<xliff:g id="NAME">%1$s</xliff:g>“"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Programa „<xliff:g id="NAME">%1$s</xliff:g>“ suarchyvuota. Palieskite, jei norite atsisiųsti ir atkurti."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Programa „<xliff:g id="NAME">%1$s</xliff:g>“ suarchyvuota."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"atsisiųsti ir atkurti"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Būtina atnaujinti programą"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Šios piktogramos programa neatnaujinta. Galite patys atnaujinti, kad iš naujo įgalintumėte šį spartųjį klavišą, arba pašalinkite piktogramą."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atnaujinti"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Supratau"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pristabdyti darbo programas"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Atšaukti pristabdymą"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtruoti"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nepavyko: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privati erdvė"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 4f1a92c..e02abe0 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Logrīki atspējoti drošajā režīmā"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Saīsne nav pieejama."</string>
     <string name="home_screen" msgid="5629429142036709174">"Sākums"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Iestatiet <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kā noklusējuma sākuma lietotni iestatījumos."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Sadalīt ekrānu"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s: informācija par lietotni"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Lietojuma iestatījumi: %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Jauns logs"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Saglabāt lietotņu pāri"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Šis lietotņu pāris netiek atbalstīts šajā ierīcē"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Lietotņu pāris nav pieejams"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Lai pārvietotu logrīku, pieskarieties un turiet."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Lai pārvietotu logrīku, uz tā veiciet dubultskārienu un turiet. Varat arī veikt pielāgotas darbības."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Citas iespējas"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Rādīt visus logrīkus"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d plats un %2$d augsts"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Logrīks <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Piezīmju pierakstīšana"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Pievienot"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Pievienot logrīku <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Pieskarieties, lai mainītu logrīka iestatījumus."</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Mainīt logrīka iestatījumus"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Meklēt lietotnes"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalēt"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Neieteikt lietotni"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Piespraust prognozēto lietotni"</string>
+    <string name="bubble" msgid="3072951361014076670">"Burbulis"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalēt saīsnes"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Ļauj lietotnei pievienot saīsnes, nejautājot lietotājam."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"sākuma ekrāna iestatījumu un saīšņu lasīšana"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Atspējojis administrators"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Atļaut sākuma ekrāna pagriešanu"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Pagriežot tālruni"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Paziņojumu punkti"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ieslēgti"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Izslēgts"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Notiek lietotnes “<xliff:g id="NAME">%1$s</xliff:g>” instalēšana. Norise: <xliff:g id="PROGRESS">%2$s</xliff:g>."</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Lietotnes <xliff:g id="NAME">%1$s</xliff:g> lejupielāde (<xliff:g id="PROGRESS">%2$s</xliff:g> pabeigti)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Notiek <xliff:g id="NAME">%1$s</xliff:g> instalēšana"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Lietotne <xliff:g id="NAME">%1$s</xliff:g> ir arhivēta; lai lejupielādētu un atjaunotu, pieskarieties"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Lietotne <xliff:g id="NAME">%1$s</xliff:g> ir arhivēta."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"lejupielādēt un atjaunot"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Lietotne ir jāatjaunina"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Šai ikonai paredzētā lietotne nav atjaunināta. Varat to atjaunināt manuāli, lai atkārtoti iespējotu šo saīsni, vai noņemt ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atjaunināt"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Labi"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pārtraukt darba lietotņu darbību"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Atsākt"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrs"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Neizdevās: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privātā telpa"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index d984ca0..e524d82 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Додатоците се оневозможени во безбеден режим"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Кратенката не е достапна"</string>
     <string name="home_screen" msgid="5629429142036709174">"Почетен екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Поставете <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> да биде стандардна апликација за почетен екран во „Поставки“"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Поделен екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Податоци за апликација за %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Поставки за користење за %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нов прозорец"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Зачувај го парот апликации"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Паров апликации не е поддржан на уредов"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Парот апликации не е достапен"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Допрете и задржете за да преместите виџет."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Допрете двапати и задржете за да преместите виџет или користете приспособени дејства."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Повеќе опции"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Прикажи ги сите виџети"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d широк на %2$d висок"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Виџет <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Фаќање белешки"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Додај"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Додај го виџетот <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Допрете за да ги промените поставките за виџетот"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Промени ги поставките за виџетот"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Пребарувајте апликации"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Инсталирај"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Не предлагај апл."</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Закачи го предвидувањето"</string>
+    <string name="bubble" msgid="3072951361014076670">"Балонче"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"инсталирање кратенки"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Овозможува апликацијата да додава кратенки без интервенција на корисникот."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"да чита поставки и кратенки на почетна страница"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Оневозможено од администраторот"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дозволи ротирање на почетниот екран"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Кога телефонот се ротира"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Точки за известување"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Вклучено"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Исклучено"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> се инсталира, <xliff:g id="PROGRESS">%2$s</xliff:g> завршено"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Се презема <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> завршено"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> чека да се инсталира"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Апликацијата <xliff:g id="NAME">%1$s</xliff:g> е архивирана. Допрете за да преземете и вратите."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Апликацијата <xliff:g id="NAME">%1$s</xliff:g> е архивирана."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"преземете и вратете"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Потребно е ажурирање на апликацијата"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Апликацијата за оваа икона не е ажурирана. Може да ажурирате рачно за да повторно се овозможи кратенкава или отстранете ја иконата."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ажурирај"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Сфатив"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Паузирај ги работните апликации"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Прекини ја паузата"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Филтер"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не успеа: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Приватен простор"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index ea8849e..e273e14 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"സുരക്ഷിത മോഡിൽ വിജറ്റുകൾ പ്രവർത്തനരഹിതമാക്കി"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"കുറുക്കുവഴി ലഭ്യമല്ല"</string>
     <string name="home_screen" msgid="5629429142036709174">"ഹോം"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ക്രമീകരണത്തിൽ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> എന്നത് ഡിഫോൾട്ട് ഹോം ആപ്പായി സജ്ജീകരിക്കുക"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"സ്‌ക്രീൻ വിഭജന മോഡ്"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s എന്നതിന്റെ ആപ്പ് വിവരങ്ങൾ"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s എന്നതിനുള്ള ഉപയോഗ ക്രമീകരണം"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"പുതിയ വിന്‍ഡോ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ആപ്പ് ജോടി സംരക്ഷിക്കുക"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ഈ ഉപകരണത്തിൽ ഈ ആപ്പ് ജോടിക്ക് പിന്തുണയില്ല"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ആപ്പ് ജോടി ലഭ്യമല്ല"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"വിജറ്റ് നീക്കാൻ സ്‌പർശിച്ച് പിടിക്കുക."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"വിജറ്റ് നീക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യൂ, ഹോൾഡ് ചെയ്യൂ അല്ലെങ്കിൽ ഇഷ്‌ടാനുസൃത പ്രവർത്തനങ്ങൾ ഉപയോഗിക്കൂ."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"കൂടുതൽ ഓപ്ഷനുകൾ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"എല്ലാ വിജറ്റും കാണിക്കുക"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d വീതിയും %2$d ഉയരവും"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> വിജറ്റ്"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"കുറിപ്പ് രേഖപ്പെടുത്തൽ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ചേർക്കുക"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> വിജറ്റ് ചേർക്കുക"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"വിജറ്റ് ക്രമീകരണം മാറ്റാൻ ടാപ്പ് ചെയ്യുക"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"വിജറ്റ് ക്രമീകരണം മാറ്റുക"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ആപ്പുകൾ തിരയുക"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ഇൻസ്‌റ്റാൾ ചെയ്യുക"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ആപ്പ് നിർദ്ദേശിക്കേണ്ട"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"പ്രവചനം പിൻ ചെയ്യുക"</string>
+    <string name="bubble" msgid="3072951361014076670">"ബബിൾ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"കുറുക്കുവഴികൾ ഇൻസ്റ്റാളുചെയ്യുക"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ഉപയോക്തൃ ഇടപെടൽ ഇല്ലാതെ കുറുക്കുവഴികൾ ചേർക്കാൻ അപ്ലിക്കേഷനെ അനുവദിക്കുന്നു."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ഹോം ക്രമീകരണവും കുറുക്കുവഴികളും വായിക്കുക"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"അഡ്മിൻ പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ഹോം സ്ക്രീൻ റൊട്ടേഷൻ അനുവദിക്കുക"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ഫോൺ തിരിച്ച നിലയിലായിരിക്കുമ്പോൾ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"അറിയിപ്പ് ഡോട്ടുകൾ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ഓണാണ്"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ഓഫാണ്"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ഇൻസ്‌റ്റാൾ ചെയ്യുന്നു, <xliff:g id="PROGRESS">%2$s</xliff:g> പൂർത്തിയായി"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ഡൗൺലോഡ് ചെയ്യുന്നു, <xliff:g id="PROGRESS">%2$s</xliff:g> പൂർത്തിയായി"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"ഇൻസ്റ്റാൾ ചെയ്യാൻ <xliff:g id="NAME">%1$s</xliff:g> കാക്കുന്നു"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ആർക്കൈവ് ചെയ്തു. ഡൗൺലോഡ് ചെയ്യാനും പുനഃസ്ഥാപിക്കാനും ടാപ്പ് ചെയ്യുക."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ആർക്കൈവ് ചെയ്തു."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ഡൗൺലോഡ് ചെയ്ത് പുനഃസ്ഥാപിക്കുക"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ആപ്പ് അപ്‌ഡേറ്റ് ചെയ്യേണ്ടതുണ്ട്"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ഈ ഐക്കണിനുള്ള ആപ്പ് അപ്‌ഡേറ്റ് ചെയ്തിട്ടില്ല. ഈ കുറുക്കുവഴി വീണ്ടും പ്രവർത്തനക്ഷമമാക്കാൻ നിങ്ങൾക്ക് നേരിട്ട് അപ്‌ഡേറ്റ് ചെയ്യാം അല്ലെങ്കിൽ ഐക്കൺ നീക്കം ചെയ്യാം."</string>
     <string name="dialog_update" msgid="2178028071796141234">"അപ്ഡേറ്റ് ചെയ്യുക"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"മനസ്സിലായി"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ഔദ്യോഗിക ആപ്പുകൾ താൽക്കാലികമായി നിർത്തുക"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"താൽക്കാലികമായി നിർത്തിയത് മാറ്റുക"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ഫിൽട്ടർ ചെയ്യുക"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"പരാജയപ്പെട്ടു: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"സ്വകാര്യ സ്പേസ്"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 8554717..7e5ed73 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Safe горимд виджетүүдийг идэвхгүйжүүлсэн"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Товчлол алга"</string>
     <string name="home_screen" msgid="5629429142036709174">"Нүүр"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>-г Тохиргоонд өгөгдмөл үндсэн аппаар тохируулна уу"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Дэлгэцийг хуваах"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s-н аппын мэдээлэл"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s-н ашиглалтын тохиргоо"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Шинэ цонх"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Апп хослуулалтыг хадгалах"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Энэ апп хослуулалтыг уг төхөөрөмж дээр дэмждэггүй"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Апп хослуулалт боломжгүй байна"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Виджетийг зөөх бол хүрээд, удаан дарна уу."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Виджетийг зөөх эсвэл захиалгат үйлдлийг ашиглахын тулд хоёр товшоод, удаан дарна уу."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Бусад сонголт"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Бүх виджетийг харуулах"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d өргөн %2$d өндөр"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> жижиг хэрэгсэл"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Тэмдэглэл хөтлөх"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Нэмэх"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджетийг нэмэх"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Жижиг хэрэгслийн тохиргоог өөрчлөхийн тулд товшино уу"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Жижиг хэрэгслийн тохиргоог өөрчлөх"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Апп хайх"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Суулгах"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Апп бүү санал болго"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Таамаглалыг бэхлэх"</string>
+    <string name="bubble" msgid="3072951361014076670">"Бөмбөлөг"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"товчлол суулгах"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Апп нь хэрэглэгчийн оролцоогүйгээр товчлолыг нэмэж чадна"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"нүүрний тохиргоо болон товчлолыг унших"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Таны админ идэвхгүй болгосон"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Үндсэн нүүрийг эргүүлэхийг зөвшөөрөх"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Утсыг эргүүлсэн үед"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Мэдэгдлийн цэг"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Асаалттай"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Унтраалттай"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g>-г суулгаж байна. <xliff:g id="PROGRESS">%2$s</xliff:g> дууссан"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g>-г татаж байна, <xliff:g id="PROGRESS">%2$s</xliff:g> татсан"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> нь суулгахыг хүлээж байна"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>-г архивласан. Татаж, сэргээхийн тулд товшино уу."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g>-г архивласан."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"татах, сэргээх"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Аппын шинэчлэлт шаардлагатай"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Энэ дүрс тэмдгийн аппыг шинэчлээгүй. Та энэ товчлолыг дахин идэвхжүүлэх эсвэл дүрсийг хасахын тулд гараар шинэчлэх боломжтой."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Шинэчлэх"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ойлголоо"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Ажлын аппуудыг түр зогсоох"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Түр зогсоохоо болих"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Шүүлтүүр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Амжилтгүй болсон: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Хувийн орон зай"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index dcdf25c..b5d4f71 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"विजेट सुरक्षित मोडमध्ये अक्षम झाले"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"शॉर्टकट उपलब्ध नाही"</string>
     <string name="home_screen" msgid="5629429142036709174">"होम"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"सेटिंग्ज मध्ये <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> हे डीफॉल्ट होम अ‍ॅप म्हणून सेट करा"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"स्प्लिट स्क्रीन"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s साठी ॲपशी संबंधित माहिती"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s साठी वापरासंबंधित सेटिंग्ज"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"नवीन विंडो"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ॲपची जोडी सेव्ह करा"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"या ॲपची जोडीला या डिव्हाइसवर सपोर्ट नाही"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ॲपची जोडी उपलब्ध नाही"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"विजेट हलवण्यासाठी स्पर्श करा आणि धरून ठेवा."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"विजेट हलवण्यासाठी किंवा कस्टम कृती वापरण्यासाठी दोनदा टॅप करा आणि धरून ठेवा."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"आणखी पर्याय"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"सर्व विजेट दाखवा"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d रूंद बाय %2$d उंच"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"टिपा घेणे"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"जोडा"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट जोडा"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेट सेटिंग्ज बदलण्यासाठी टॅप करा"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेट सेटिंग्ज बदला"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"अ‍ॅप्स शोधा"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"इंस्टॉल करा"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ॲप सुचवू नका"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"पूर्वानुमान पिन करा"</string>
+    <string name="bubble" msgid="3072951361014076670">"बबल"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"शॉर्टकट इंस्टॉल करा"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"वापरकर्ता हस्तक्षेपाशिवाय शॉर्टकट जोडण्यास अ‍ॅप ला अनुमती देते."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"होम सेटिंग्ज आणि शॉर्टकट वाचा"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"आपल्या प्रशासकाने अक्षम केले"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"होम स्क्रीन फिरवण्‍याची अनुमती द्या"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"फोन फिरवला जातो तेव्हा"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"नोटिफिकेशन डॉट"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"सुरू"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"बंद"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> इंस्टॉल करत आहे, <xliff:g id="PROGRESS">%2$s</xliff:g> पूर्ण झाले"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> डाउनलोड होत आहे , <xliff:g id="PROGRESS">%2$s</xliff:g> पूर्ण झाले"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> इंस्टॉल करण्याची प्रतिक्षा करत आहे"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> संग्रहित केले आहे. डाउनलोड करून रिस्टोअर करण्यासाठी टॅप करा."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> संग्रहित केले आहे."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"डाउनलोड करून रिस्टोअर करा"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"अ‍ॅप अपडेट करणे आवश्‍यक आहे"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"या आयकनसाठी अ‍ॅप अपडेट केलेले नाही. हा शॉटकर्ट पुन्हा सुरू करण्यासाठी तुम्ही मॅन्युअली अपडेट करू शकता किंवा आयकन काढून टाका."</string>
     <string name="dialog_update" msgid="2178028071796141234">"अपडेट करा"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"समजले"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"कार्य ॲप्स थांबवा"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"अनपॉझ करा"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"फिल्टर"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"हे करता आले नाही: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"खाजगी स्पेस"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 22ee2cb..a48d518 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widget dilumpuhkan dalam mod Selamat"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Pintasan tidak tersedia"</string>
     <string name="home_screen" msgid="5629429142036709174">"Rumah"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Tetapkan <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> sebagai apl skrin utama lalai dalam Tetapan"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Skrin pisah"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Maklumat apl untuk %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Tetapan penggunaan sebanyak %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Tetingkap Baharu"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Simpan gandingan apl"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Gandingan apl ini tidak disokong pada peranti ini"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Gandingan apl tidak tersedia"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Sentuh &amp; tahan untuk menggerakkan widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ketik dua kali &amp; tahan untuk menggerakkan widget atau menggunakan tindakan tersuai."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Lagi pilihan"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Tunjukkan semua widget"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Lebar %1$d kali tinggi %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -62,11 +64,17 @@
     <string name="no_widgets_available" msgid="4337693382501046170">"Widget dan pintasan tidak tersedia"</string>
     <string name="no_search_results" msgid="3787956167293097509">"Tiada widget atau pintasan yang dijumpai"</string>
     <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Peribadi"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Tempat kerja"</string>
+    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Kerja"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"Perbualan"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pengambilan nota"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Tambah"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Tambahkan widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ketik untuk menukar tetapan widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Tukar tetapan widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Cari apl"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Pasang"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Jangan cadangkan apl"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Sematkan Ramalan"</string>
+    <string name="bubble" msgid="3072951361014076670">"Gelembung"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"pasang pintasan"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Membenarkan apl menambah pintasan tanpa campur tangan pengguna."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"membaca tetapan dan pintasan skrin utama"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Dilumpuhkan oleh pentadbir anda"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Benarkan putaran skrin utama"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Apabila telefon diputar"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Titik pemberitahuan"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Hidup"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Mati"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> dipasang, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> memuat turun, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> menunggu untuk dipasang"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> diarkibkan. Ketik untuk memuat turun dan memulihkan apl tersebut."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> diarkibkan."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"muat turun dan pulihkan"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kemas kini apl diperlukan"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Apl untuk ikon ini tidak dikemas kini. Anda boleh mengemas kini secara manual untuk mendayakan semula pintasan atau mengalih keluar ikon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Kemas kini"</string>
@@ -184,9 +198,11 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Jeda apl kerja"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Nyahjeda"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Tapis"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Gagal: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
-    <string name="private_space_label" msgid="2359721649407947001">"Ruang privasi"</string>
+    <string name="private_space_label" msgid="2359721649407947001">"Ruang persendirian"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Ketik untuk menyediakan atau membuka"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Persendirian"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Tetapan Ruang Peribadi"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index bca1c6e..f892a1b 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"လုံခြုံရေး မုဒ်ထဲမှာ ဝီဂျက်များကို ပိတ်ထား"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ဖြတ်လမ်း မရနိုင်ပါ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ပင်မစာမျက်နှာ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ဆက်တင်များတွင် <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ကို မူရင်းပင်မစာမျက်နှာအက်ပ်အဖြစ် သတ်မှတ်ရန်"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"မျက်နှာပြင် ခွဲ၍ပြသခြင်း"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s အတွက် အက်ပ်အချက်အလက်"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s အတွက် အသုံးပြုမှုဆက်တင်များ"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ဝင်းဒိုးအသစ်"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"အက်ပ်တွဲချိတ်ခြင်း သိမ်းရန်"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ဤအက်ပ်တွဲချိတ်ခြင်းကို ဤစက်တွင် ပံ့ပိုးမထားပါ"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"အက်ပ်တွဲချိတ်ခြင်းကို မရနိုင်ပါ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ဝိဂျက်ကို ရွှေ့ရန် တို့ပြီး ဖိထားပါ။"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ဝိဂျက်ကို ရွှေ့ရန် (သို့) စိတ်ကြိုက်လုပ်ဆောင်ချက်များကို သုံးရန် နှစ်ချက်တို့ပြီး ဖိထားပါ။"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"နောက်ထပ် ရွေးစရာများ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ဝိဂျက်အားလုံး ပြပါ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"အလျား %1$d နှင့် အမြင့် %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ဝိဂျက်"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"မှတ်စုလိုက်ခြင်း"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ထည့်ရန်"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ဝိဂျက်ထည့်ရန်"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ဝိဂျက် ဆက်တင်များကို ပြောင်းရန် တို့ပါ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ဝိဂျက် ဆက်တင်များကို ပြောင်းပါ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ရှာဖွေမှု အက်ပ်များ"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ထည့်သွင်းရန်"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"အက်ပ်အကြံမပြုပါနှင့်"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ခန့်မှန်းချက်ကို ပင်ထိုးရန်"</string>
+    <string name="bubble" msgid="3072951361014076670">"ပူဖောင်းကွက်"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ဖြတ်လမ်းလင့်ခ်များ ထည့်သွင်းခြင်း"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"အသုံးပြုသူ လုပ်ဆောင်မှုမရှိပဲ အပ်ပလီကေးရှင်းကို အတိုကောက်မှတ်သားမှုများ ပြုလုပ်ခွင့် ပေးခြင်း"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ပင်မဆက်တင်နှင့် ဖြတ်လမ်းလင့်ခ်များ ဖတ်ခြင်း"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"သင့်စီမံခန့်ခွဲသူက ပိတ်လိုက်ပါသည်"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ပင်မစာမျက်နှာလှည့်ခြင်းကို ခွင့်ပြုခြင်း"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ဖုန်းကိုလှည့်ထားစဉ်"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"သတိပေးချက် အစက်များ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ဖွင့်"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ပိတ်"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ကို ထည့်သွင်းနေသည်၊ <xliff:g id="PROGRESS">%2$s</xliff:g> ပြီးပါပြီ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ဒေါင်းလုဒ်လုပ်နေသည်၊ <xliff:g id="PROGRESS">%2$s</xliff:g> ပြီးပါပြီ"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ကိုထည့်သွင်းရန်စောင့်နေသည်"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ကို သိမ်းထားသည်။ ဒေါင်းလုဒ်လုပ်ပြီး ပြန်ယူရန် တို့ပါ။"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ကို သိမ်းထားသည်။"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ဒေါင်းလုဒ်လုပ်ပြီး ပြန်ယူရန်"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"အက်ပ်ကို အပ်ဒိတ်လုပ်ရန် လိုအပ်သည်"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ဤသင်္ကေတအတွက် အက်ပ်ကို အပ်ဒိတ်လုပ်မထားပါ။ ဤဖြတ်လမ်းလင့်ခ်ကို ပြန်ဖွင့်ရန် ကိုယ်တိုင်အပ်ဒိတ်လုပ်နိုင်သည် (သို့) သင်္ကေတကို ဖယ်ရှားနိုင်သည်။"</string>
     <string name="dialog_update" msgid="2178028071796141234">"အပ်ဒိတ်လုပ်ရန်"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"နားလည်ပြီ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"အလုပ်သုံးအက်ပ်များကို ခဏရပ်ရန်"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ပြန်ဖွင့်ရန်"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"စစ်ထုတ်ရန်"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"မအောင်မြင်ပါ− <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"သီးသန့်ချတ်ခန်း"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 1440443..b9b69a0 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -27,18 +27,20 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Moduler er deaktivert i sikker modus"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Snarveien er ikke tilgjengelig"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startskjerm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Angi <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> som standard startsideapp i innstillingene"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Delt skjerm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Appinformasjon for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Bruksinnstillinger for %1$s"</string>
-    <string name="save_app_pair" msgid="5647523853662686243">"Lagre apptilkoblingen"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nytt vindu"</string>
+    <string name="save_app_pair" msgid="5647523853662686243">"Lagre app-paret"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Denne apptilkoblingen støttes ikke på denne enheten"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Åpne enheten for å bruke denne apptilkoblingen"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"Apptilkoblingen er ikke tilgjengelig"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Trykk og hold for å flytte en modul."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dobbelttrykk og hold inne for å flytte en modul eller bruke tilpassede handlinger."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Flere alternativer"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Vis alle moduler"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d bredde x %2$d høyde"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>-modul"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notatskriving"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Legg til"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Legg til <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-modulen"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Trykk for å endre modulinnstillinger"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Endre modulinnstillinger"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Søk etter apper"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installer"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ikke foreslå app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fest forslaget"</string>
+    <string name="bubble" msgid="3072951361014076670">"Boble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installere snarveier"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Gir apper tillatelse til å legge til snarveier uten innblanding fra brukeren."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"lese startsideinnstillinger og -snarveier"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administratoren har slått av funksjonen"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Tillat at startskjermen roterer"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Når telefonen roteres"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Varselsprikker"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"På"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Av"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installerer, <xliff:g id="PROGRESS">%2$s</xliff:g> er fullført"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Laster ned <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> er fullført"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Venter på å installere <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> er arkivert. Trykk for å laste ned og gjenopprette."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> er arkivert."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"last ned og gjenopprett"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Appen må oppdateres"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Appen for dette ikonet er ikke oppdatert. Du kan oppdatere manuelt for å aktivere denne snarveien igjen, eller du kan fjerne ikonet."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Oppdater"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Greit"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Sett jobbapper på pause"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Gjenoppta"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mislyktes: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privat område"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 52613cb..6f93761 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"सुरक्षित मोडमा विगेटहरू अक्षम गरियो"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"सर्टकट उपलब्ध छैन"</string>
     <string name="home_screen" msgid="5629429142036709174">"होम"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"सेटिङमा गई <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> लाई डिफल्ट होम एपका रूपमा सेट गर्नुहोस्"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"स्प्लिट स्क्रिन"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s का हकमा एपसम्बन्धी जानकारी"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s को प्रयोगसम्बन्धी सेटिङ"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"नयाँ विन्डो"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"एपको पेयर सेभ गर्नुहोस्"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"यस डिभाइसमा यो एप पेयर प्रयोग गर्न मिल्दैन"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"एप पेयर उपलब्ध छैन"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"कुनै विजेट सार्न डबल ट्याप गरेर छोइराख्नुहोस्।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"कुनै विजेट सार्न वा आफ्नो रोजाइका कारबाही प्रयोग गर्न डबल ट्याप गरेर छोइराख्नुहोस्।"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"थप विकल्पहरू"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"सबै विजेटहरू देखाउनुहोस्"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d चौडाइ गुणा %2$d उचाइ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"नोट लेख्ने कार्य"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"हाल्नुहोस्"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट हाल्नुहोस्"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेटका सेटिङ बदल्न ट्याप गर्नुहोस्"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेटका सेटिङ बदल्नुहोस्"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"एपहरू खोज्नुहोस्"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"स्थापना गर्नुहोस्"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"एप सिफारिस नगर्नुहोस्"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"सिफारिस गरिएको एप पिन गर्नुहोस्"</string>
+    <string name="bubble" msgid="3072951361014076670">"बबल"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"सर्टकट स्थापना गर्नेहोस्"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"प्रयोगकर्ताको हस्तक्षेप बिना एउटा एपलाई सर्टकटमा थप्नको लागि अनुमति दिनुहोस्।"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"होम स्क्रिनका सेटिङ र सर्टकटहरू रिड गर्नुहोस्"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"तपाईँको प्रशासकद्वारा असक्षम गरिएको"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"होम स्क्रिन रोटेट हुन दिइयोस्"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"फोन घुमाउँदा"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"नोटिफिकेसन डट"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"सक्रिय"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"निष्क्रिय"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> इन्स्टल गरिँदै छ, <xliff:g id="PROGRESS">%2$s</xliff:g> पूरा भयो"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> डाउनलोड गर्दै, <xliff:g id="PROGRESS">%2$s</xliff:g> सम्पन्‍न"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> स्थापना गर्न प्रतीक्षा गर्दै"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> अभिलेखमा राखिएको छ। डाउनलोड गरी रिस्टोर गर्न ट्याप गर्नुहोस्।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> अभिलेखमा राखिएको छ।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"डाउनलोड र रिस्टोर गर्नुहोस्"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"एप अपडेट गरिनु पर्छ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"यो आइकनले जनाउने एप अपडेट गरिएको छैन। तपाईं यो सर्टकट फेरि अन गर्न म्यानुअल रूपमा अपडेट गर्न सक्नुहुन्छ वा आइकन नै हटाउनुहोस्।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"अपडेट गर्नुहोस्"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"बुझेँ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"कामसम्बन्धी एपहरू पज गर्नुहोस्"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"सुचारु गर्नुहोस्"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"फिल्टर"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"कार्य पूरा गर्न सकिएन: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"निजी स्पेस"</string>
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index 2688b83..d9f9769 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -53,43 +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_secondary_fixed_variant">@android:color/system_accent2_700</color>
-    <color name="material_color_on_tertiary_fixed_variant">@android:color/system_accent3_700</color>
-    <color name="material_color_on_primary_fixed_variant">@android:color/system_accent1_700</color>
-    <color name="material_color_on_secondary_container">@android:color/system_accent2_100</color>
-    <color name="material_color_on_tertiary_container">@android:color/system_accent3_100</color>
-    <color name="material_color_on_primary_container">@android:color/system_accent1_100</color>
-    <color name="material_color_secondary_fixed_dim">@android:color/system_accent2_200</color>
-    <color name="material_color_on_error_container">#FFDAD5</color>
-    <color name="material_color_on_secondary_fixed">@android:color/system_accent2_900</color>
-    <color name="material_color_on_surface_inverse">@android:color/system_neutral1_900</color>
-    <color name="material_color_tertiary_fixed_dim">@android:color/system_accent3_200</color>
-    <color name="material_color_on_tertiary_fixed">@android:color/system_accent3_900</color>
-    <color name="material_color_primary_fixed_dim">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary_container">@android:color/system_accent2_700</color>
-    <color name="material_color_error_container">#930001</color>
-    <color name="material_color_on_primary_fixed">@android:color/system_accent1_900</color>
-    <color name="material_color_primary_inverse">@android:color/system_accent1_600</color>
-    <color name="material_color_secondary_fixed">@android:color/system_accent2_100</color>
-    <color name="material_color_tertiary_container">@android:color/system_accent3_700</color>
-    <color name="material_color_tertiary_fixed">@android:color/system_accent3_100</color>
-    <color name="material_color_primary_container">@android:color/system_accent1_700</color>
-    <color name="material_color_on_background">@android:color/system_neutral1_800</color>
-    <color name="material_color_primary_fixed">@android:color/system_accent1_100</color>
-    <color name="material_color_on_secondary">@android:color/system_accent2_800</color>
-    <color name="material_color_on_tertiary">@android:color/system_accent3_800</color>
-    <color name="material_color_on_error">#690001</color>
-    <color name="material_color_on_surface_variant">@android:color/system_neutral2_200</color>
-    <color name="material_color_outline">@android:color/system_neutral2_400</color>
-    <color name="material_color_outline_variant">@android:color/system_neutral2_700</color>
-    <color name="material_color_on_primary">@android:color/system_accent1_800</color>
     <color name="material_color_on_surface">@android:color/system_neutral1_100</color>
-    <color name="material_color_primary">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary">@android:color/system_accent2_200</color>
-    <color name="material_color_tertiary">@android:color/system_accent3_200</color>
 </resources>
\ No newline at end of file
diff --git a/res/values-night-v34/colors.xml b/res/values-night-v34/colors.xml
index af28119..abce763 100644
--- a/res/values-night-v34/colors.xml
+++ b/res/values-night-v34/colors.xml
@@ -27,4 +27,7 @@
         @android:color/system_on_surface_dark</color>
     <color name="widget_cell_subtitle_color_dark">
         @android:color/system_on_surface_variant_dark</color>
+    <color name="widget_picker_menu_options_color_dark">
+        @android:color/system_on_surface_variant_dark
+    </color>
 </resources>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
index 95b3a63..887a2a5 100644
--- a/res/values-night/colors.xml
+++ b/res/values-night/colors.xml
@@ -1,64 +1,20 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 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.
-*/
--->
+<?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.
+  -->
+
 <resources>
-    <color name="material_color_on_secondary_fixed_variant">#3F4759</color>
-    <color name="material_color_on_tertiary_fixed_variant">#583E5B</color>
     <color name="material_color_surface_container_lowest">#0D0E11</color>
-    <color name="material_color_on_primary_fixed_variant">#2B4678</color>
-    <color name="material_color_on_secondary_container">#DBE2F9</color>
-    <color name="material_color_on_tertiary_container">#FBD7FC</color>
-    <color name="material_color_surface_container_low">#1B1B1F</color>
-    <color name="material_color_on_primary_container">#D8E2FF</color>
-    <color name="material_color_secondary_fixed_dim">#BFC6DC</color>
-    <color name="material_color_on_error_container">#FFDAD5</color>
-    <color name="material_color_on_secondary_fixed">#141B2C</color>
-    <color name="material_color_on_surface_inverse">#1B1B1F</color>
-    <color name="material_color_tertiary_fixed_dim">#DEBCDF</color>
-    <color name="material_color_on_tertiary_fixed">#29132D</color>
-    <color name="material_color_primary_fixed_dim">#ADC6FF</color>
-    <color name="material_color_secondary_container">#3F4759</color>
-    <color name="material_color_error_container">#930001</color>
-    <color name="material_color_on_primary_fixed">#001A41</color>
-    <color name="material_color_primary_inverse">#445E91</color>
-    <color name="material_color_secondary_fixed">#DBE2F9</color>
-    <color name="material_color_surface_inverse">#FAF9FD</color>
-    <color name="material_color_surface_variant">#44474F</color>
-    <color name="material_color_tertiary_container">#583E5B</color>
-    <color name="material_color_tertiary_fixed">#FBD7FC</color>
-    <color name="material_color_primary_container">#2B4678</color>
-    <color name="material_color_on_background">#E3E2E6</color>
-    <color name="material_color_primary_fixed">#D8E2FF</color>
-    <color name="material_color_on_secondary">#293041</color>
-    <color name="material_color_on_tertiary">#402843</color>
-    <color name="material_color_surface_dim">#121316</color>
-    <color name="material_color_surface_bright">#38393C</color>
-    <color name="material_color_on_error">#690001</color>
-    <color name="material_color_surface">#121316</color>
-    <color name="material_color_surface_container_high">#292A2D</color>
-    <color name="material_color_surface_container_highest">#343538</color>
-    <color name="material_color_on_surface_variant">#C4C6D0</color>
-    <color name="material_color_outline">#72747D</color>
-    <color name="material_color_outline_variant">#444746</color>
-    <color name="material_color_on_primary">#102F60</color>
     <color name="material_color_on_surface">#E3E2E6</color>
-    <color name="material_color_surface_container">#1F1F23</color>
-    <color name="material_color_primary">#ADC6FF</color>
-    <color name="material_color_secondary">#BFC6DC</color>
-    <color name="material_color_tertiary">#DEBCDF</color>
-</resources>
+</resources>
\ No newline at end of file
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index c95722f..06f0eee 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -26,9 +26,64 @@
         <item name="android:backgroundDimEnabled">true</item>
     </style>
 
-    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+    <style name="DynamicColorsBaseLauncherTheme" parent="@style/BaseLauncherTheme">
+        <item name="materialColorOnSecondaryFixedVariant">@color/system_on_secondary_fixed_variant</item>
+        <item name="materialColorOnTertiaryFixedVariant">@color/system_on_tertiary_fixed_variant</item>
+        <item name="materialColorSurfaceContainerLowest">@color/system_surface_container_lowest_dark</item>
+        <item name="materialColorOnPrimaryFixedVariant">@color/system_on_primary_fixed_variant</item>
+        <item name="materialColorOnSecondaryContainer">@color/system_on_secondary_container_dark</item>
+        <item name="materialColorOnTertiaryContainer">@color/system_on_tertiary_container_dark</item>
+        <item name="materialColorSurfaceContainerLow">@color/system_surface_container_low_dark</item>
+        <item name="materialColorOnPrimaryContainer">@color/system_on_primary_container_dark</item>
+        <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="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="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
+        <item name="materialColorSurfaceInverse">@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>
+        <item name="materialColorPrimaryContainer">@color/system_primary_container_dark</item>
+        <item name="materialColorOnBackground">@color/system_on_background_dark</item>
+        <item name="materialColorPrimaryFixed">@color/system_primary_fixed</item>
+        <item name="materialColorOnSecondary">@color/system_on_secondary_dark</item>
+        <item name="materialColorOnTertiary">@color/system_on_tertiary_dark</item>
+        <item name="materialColorSurfaceDim">@color/system_surface_dim_dark</item>
+        <item name="materialColorSurfaceBright">@color/system_surface_bright_dark</item>
+        <item name="materialColorOnError">@color/system_on_error_dark</item>
+        <item name="materialColorSurface">@color/system_surface_dark</item>
+        <item name="materialColorSurfaceContainerHigh">@color/system_surface_container_high_dark</item>
+        <item name="materialColorSurfaceContainerHighest">@color/system_surface_container_highest_dark</item>
+        <item name="materialColorOnSurfaceVariant">@color/system_on_surface_variant_dark</item>
+        <item name="materialColorOutline">@color/system_outline_dark</item>
+        <item name="materialColorOutlineVariant">@color/system_outline_variant_dark</item>
+        <item name="materialColorOnPrimary">@color/system_on_primary_dark</item>
+        <item name="materialColorOnSurface">@color/system_on_surface_dark</item>
+        <item name="materialColorSurfaceContainer">@color/system_surface_container_dark</item>
+        <item name="materialColorPrimary">@color/system_primary_dark</item>
+        <item name="materialColorSecondary">@color/system_secondary_dark</item>
+        <item name="materialColorTertiary">@color/system_tertiary_dark</item>
+        <item name="materialColorError">@color/system_error_dark</item>
+    </style>
+
+    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation</item>
+
+        <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="pageIndicatorDotColor">@color/page_indicator_dot_color_dark</item>
     </style>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 90dcd46..a38971f 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets uitgezet in veilige modus"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Snelkoppeling is niet beschikbaar"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startscherm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Stel <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> in als de standaard startscherm-app in Instellingen"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Gesplitst scherm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App-info voor %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Gebruiksinstellingen voor %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nieuw venster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"App-paar opslaan"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Dit app-paar wordt niet ondersteund op dit apparaat"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App-paar is niet beschikbaar"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tik en houd vast om een widget te verplaatsen."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dubbeltik en houd vast om een widget te verplaatsen of aangepaste acties te gebruiken."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Meer opties"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Alle widgets tonen"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d breed en %2$d hoog"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Aantekeningen maken"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Toevoegen"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g> toevoegen"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tik om de widgetinstellingen te wijzigen"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Widgetinstellingen wijzigen"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Apps zoeken"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installeren"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Geen app voorstellen"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Voorspelling vastzetten"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubbel"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Snelle links instellen"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Een app toestaan snelkoppelingen toe te voegen zonder tussenkomst van de gebruiker."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"instellingen en snelkoppelingen op startscherm lezen"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Uitgezet door je beheerder"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Draaien van startscherm toestaan"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Als de telefoon gedraaid is"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Meldingsstipjes"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aan"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Uit"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeren, <xliff:g id="PROGRESS">%2$s</xliff:g> voltooid"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> wordt gedownload, <xliff:g id="PROGRESS">%2$s</xliff:g> voltooid"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> wacht op installatie"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is gearchiveerd. Tik om te downloaden en te herstellen."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is gearchiveerd."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"downloaden en herstellen"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App-update vereist"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"De app voor dit icoon is niet geüpdatet. Je kunt handmatig updaten om deze snelkoppeling weer aan te zetten of het icoon verwijderen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Updaten"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Werk-apps pauzeren"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Hervatten"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filteren"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mislukt: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privéruimte"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 6d2f87e..a20e0fc 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ନିରାପଦ ମୋଡରେ ୱିଜେଟ୍‌ ଅକ୍ଷମ କରାଗଲା"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ଶର୍ଟକଟ୍‌ ଉପଲବ୍ଧ ନାହିଁ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ହୋମ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ସେଟିଂସରେ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>କୁ ଡିଫଲ୍ଟ Home ଆପ ଭାବରେ ସେଟ କରନ୍ତୁ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ସ୍କ୍ରିନ‌କୁ ସ୍ପ୍ଲିଟ କରନ୍ତୁ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ପାଇଁ ଆପ ସୂଚନା"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ପାଇଁ ବ୍ୟବହାର ସେଟିଂସ"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ନୂଆ ୱିଣ୍ଡୋ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ଆପ ପେୟାର ସେଭ କରନ୍ତୁ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ଏହି ଆପ ପେୟାର ଏ ଡିଭାଇସରେ ସମର୍ଥିତ ନୁହେଁ"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ଆପ ପେୟାର ଉପଲବ୍ଧ ନାହିଁ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ଏକ ୱିଜେଟକୁ ମୁଭ୍ କରିବା ପାଇଁ ସ୍ପର୍ଶ କରି ଧରି ରଖନ୍ତୁ।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ଏକ ୱିଜେଟକୁ ମୁଭ୍ କରିବା ପାଇଁ ଦୁଇଥର-ଟାପ୍ କରି ଧରି ରଖନ୍ତୁ କିମ୍ବା କଷ୍ଟମ୍ କାର୍ଯ୍ୟଗୁଡ଼ିକୁ ବ୍ୟବହାର କରନ୍ତୁ।"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ଅଧିକ ବିକଳ୍ପ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ସମସ୍ତ ୱିଜେଟ ଦେଖାନ୍ତୁ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ଓସାର ଓ %2$d ଉଚ୍ଚ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ୱିଜେଟ୍"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ନୋଟ-ଟେକିଂ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ଯୋଗ କରନ୍ତୁ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ୱିଜେଟ ଯୋଗ କରନ୍ତୁ"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ୱିଜେଟ ସେଟିଂସ ପରିବର୍ତ୍ତନ କରିବାକୁ ଟାପ କରନ୍ତୁ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ୱିଜେଟ ସେଟିଂସ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ଆପ ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ଇନଷ୍ଟଲ୍‌ କରନ୍ତୁ"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ଆପ ପରାମର୍ଶ ଦିଅନ୍ତୁ ନାହିଁ"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ପୂର୍ବାନୁମାନକୁ ପିନ୍ କରନ୍ତୁ"</string>
+    <string name="bubble" msgid="3072951361014076670">"ବବଲ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ସର୍ଟକଟ୍‍ ଇନଷ୍ଟଲ୍‌ କରନ୍ତୁ"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ୟୁଜରଙ୍କ ବିନା ହସ୍ତକ୍ଷେପରେ ଶର୍ଟକଟ୍‌ ଯୋଡ଼ିବାକୁ ଆପକୁ ଅନୁମତି ଦିଏ।"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ହୋମ ସେଟିଂସ ଏବଂ ସର୍ଟକଟଗୁଡ଼ିକୁ ପଢ଼ନ୍ତୁ"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ଆପଣଙ୍କ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଅକ୍ଷମ କରାଯାଇଛି"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ହୋମ ସ୍କ୍ରିନ ରୋଟେସନକୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ଯେତେବେଳେ ଫୋନକୁ ରୋଟେଟ କରାଯାଇଥାଏ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ବିଜ୍ଞପ୍ତି ଡଟ୍ସ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ଚାଲୁ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ବନ୍ଦ କରନ୍ତୁ"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ଇନଷ୍ଟଲ୍ କରାଯାଉଛି, <xliff:g id="PROGRESS">%2$s</xliff:g> ସମ୍ପୂର୍ଣ୍ଣ ହୋଇଛି"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ଡାଉନଲୋଡ୍‌ ହେଉଛି, <xliff:g id="PROGRESS">%2$s</xliff:g> ସମ୍ପୂର୍ଣ୍ଣ"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ଇନଷ୍ଟଲ୍‌ ହେବାକୁ ଅପେକ୍ଷା କରିଛି"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>କୁ ଆର୍କାଇଭ କରାଯାଇଛି। ଡାଉନଲୋଡ ଏବଂ ରିଷ୍ଟୋର କରିବା ପାଇଁ ଟାପ କରନ୍ତୁ।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g>କୁ ଆର୍କାଇଭ କରାଯାଇଛି।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ଡାଉନଲୋଡ ଏବଂ ରିଷ୍ଟୋର କରନ୍ତୁ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ଆପକୁ ଅପଡେଟ କରିବା ଆବଶ୍ୟକ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ଏହି ଆଇକନ ପାଇଁ ଆପକୁ ଅପଡେଟ କରାଯାଇନାହିଁ। ଏହି ସର୍ଟକଟକୁ ପୁଣି-ସକ୍ଷମ କରିବା ପାଇଁ ଆପଣ ମାନୁଆଲୀ ଅପଡେଟ କରିପାରିବେ କିମ୍ବା ଆଇକନଟିକୁ କାଢ଼ି ଦେଇପାରିବେ।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"ଅପଡେଟ କରନ୍ତୁ"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ବୁଝିଗଲି"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ୱାର୍କ ଆପ୍ସ ବିରତ କରନ୍ତୁ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ପୁଣି ଚାଲୁ କରନ୍ତୁ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ଫିଲ୍ଟର୍"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ବିଫଳ ହୋଇଛି: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ପ୍ରାଇଭେଟ ସ୍ପେସ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 8cb5515..65187bf 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ਵਿਜੇਟ ਸੁਰੱਖਿਅਤ ਮੋਡ ਵਿੱਚ ਅਸਮਰਥਿਤ"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ਸ਼ਾਰਟਕੱਟ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ਮੁੱਖ ਪੰਨਾ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਜਾ ਕੇ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ਨੂੰ ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਹੋਮ ਐਪ ਵਜੋਂ ਸੈੱਟ ਕਰੋ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ਲਈ ਐਪ ਜਾਣਕਾਰੀ"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ਲਈ ਵਰਤੋਂ ਸੈਟਿੰਗਾਂ"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ਨਵੀਂ ਵਿੰਡੋ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ਐਪ ਜੋੜਾਬੱਧ ਰੱਖਿਅਤ ਕਰੋ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ਇਸ ਐਪ ਜੋੜਾਬੱਧ ਦਾ ਇਸ ਡੀਵਾਈਸ \'ਤੇ ਸਮਰਥਨ ਨਹੀਂ ਕੀਤਾ ਜਾਂਦਾ"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ਐਪ ਜੋੜਾਬੱਧ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ਕਿਸੇ ਵਿਜੇਟ ਨੂੰ ਲਿਜਾਉਣ ਲਈ ਸਪਰਸ਼ ਕਰਕੇ ਰੱਖੋ।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ਵਿਜੇਟ ਲਿਜਾਉਣ ਲਈ ਜਾਂ ਵਿਉਂਂਤੀਆਂ ਕਾਰਵਾਈਆਂ ਵਰਤਣ ਲਈ ਦੋ ਵਾਰ ਟੈਪ ਕਰਕੇ ਦਬਾ ਕੇ ਰੱਖੋ।"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ਹੋਰ ਵਿਕਲਪ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ਸਭ ਵਿਜੇਟ ਦਿਖਾਓ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ਚੌੜਾਈ ਅਤੇ %2$d ਲੰਬਾਈ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ਵਿਜੇਟ"</string>
@@ -62,11 +64,17 @@
     <string name="no_widgets_available" msgid="4337693382501046170">"ਵਿਜੇਟ ਜਾਂ ਸ਼ਾਰਟਕੱਟ ਉਪਲਬਧ ਨਹੀਂ ਹਨ"</string>
     <string name="no_search_results" msgid="3787956167293097509">"ਕੋਈ ਵੀ ਵਿਜੇਟ ਜਾਂ ਸ਼ਾਰਟਕੱਟ ਨਹੀਂ ਮਿਲਿਆ"</string>
     <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"ਨਿੱਜੀ"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ਕਾਰਜ-ਸਥਾਨ"</string>
+    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ਕੰਮ"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"ਗੱਲਾਂਬਾਤਾਂ"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ਨੋਟ ਬਣਾਉਣਾ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ਸ਼ਾਮਲ ਕਰੋ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ਵਿਜੇਟ ਸ਼ਾਮਲ ਕਰੋ"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ਵਿਜੇਟ ਸੈਟਿੰਗਾਂ ਨੂੰ ਬਦਲਣ ਲਈ ਟੈਪ ਕਰੋ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ਵਿਜੇਟ ਸੈਟਿੰਗਾਂ ਬਦਲੋ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ਐਪਾਂ ਖੋਜੋ"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ਸਥਾਪਤ ਕਰੋ"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ਐਪ ਦਾ ਸੁਝਾਅ ਨਾ ਦਿਓ"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ਪੂਰਵ-ਅਨੁਮਾਨ ਪਿੰਨ ਕਰੋ"</string>
+    <string name="bubble" msgid="3072951361014076670">"ਬਬਲ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ਸ਼ਾਰਟਕੱਟ ਸਥਾਪਤ ਕਰੋ"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"ਇੱਕ ਐਪ ਨੂੰ ਵਰਤੋਂਕਾਰ ਦੇ ਦਖ਼ਲ ਤੋਂ ਬਿਨਾਂ ਸ਼ਾਰਟਕੱਟ ਸ਼ਾਮਲ ਕਰਨ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ।"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ਹੋਮ ਸੈਟਿੰਗਾਂ ਅਤੇ ਸ਼ਾਰਟਕੱਟ ਪੜ੍ਹੋ"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ਤੁਹਾਡੇ ਪ੍ਰਸ਼ਾਸਕ ਦੁਆਰਾ ਅਯੋਗ ਬਣਾਈ ਗਈ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ਹੋਮ ਸਕ੍ਰੀਨ ਨੂੰ ਘੁਮਾਉਣ ਦੀ ਆਗਿਆ ਦਿਓ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ਜਦੋਂ ਫ਼ੋਨ ਘੁਮਾਇਆ ਜਾਂਦਾ ਹੈ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ਸੂਚਨਾ ਬਿੰਦੂ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ਚਾਲੂ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ਬੰਦ"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ਨੂੰ ਸਥਾਪਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ, <xliff:g id="PROGRESS">%2$s</xliff:g> ਪੂਰਾ ਹੋਇਆ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ਡਾਉਨਲੋਡ ਹੋਰ ਰਿਹਾ ਹੈ, <xliff:g id="PROGRESS">%2$s</xliff:g> ਸੰਪੂਰਣ"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ਸਥਾਪਤ ਕਰਨ ਦੀ ਉਡੀਕ ਕਰ ਰਿਹਾ ਹੈ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ਪੁਰਾਲੇਖਬੱਧ ਹੈ। ਡਾਊਨਲੋਡ ਅਤੇ ਮੁੜ-ਬਹਾਲ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ਪੁਰਾਲੇਖਬੱਧ ਹੈ।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ਡਾਊਨਲੋਡ ਕਰ ਕੇ ਮੁੜ-ਬਹਾਲ ਕਰੋ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ਐਪ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ਇਸ ਪ੍ਰਤੀਕ ਲਈ ਐਪ ਨੂੰ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਹੈ। ਇਸ ਸ਼ਾਰਟਕੱਟ ਨੂੰ ਮੁੜ-ਚਾਲੂ ਕਰਨ ਜਾਂ ਪ੍ਰਤੀਕ ਨੂੰ ਹਟਾਉਣ ਲਈ ਤੁਸੀਂ ਹੱਥੀਂ ਅੱਪਡੇਟ ਕਰ ਸਕਦੇ ਹੋ।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"ਅੱਪਡੇਟ ਕਰੋ"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ਸਮਝ ਲਿਆ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਰੋਕੋ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ਰੋਕ ਹਟਾਓ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ਫਿਲਟਰ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ਇਹ ਕਾਰਵਾਈ ਅਸਫਲ ਹੋਈ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 685b7d9..861be1e 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widżety są wyłączone w trybie bezpiecznym"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Skrót nie jest dostępny"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ekran główny"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Otwórz Ustawienia i ustaw aplikację <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> jako domyślną aplikację ekranu głównego"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podziel ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacje o aplikacji: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s – ustawienia użycia"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nowe okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Zapisz parę aplikacji"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ta para aplikacji nie jest obsługiwana na tym urządzeniu"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Para aplikacji jest niedostępna"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Naciśnij i przytrzymaj, aby przenieść widżet."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Naciśnij dwukrotnie i przytrzymaj, aby przenieść widżet lub użyć działań niestandardowych."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Więcej opcji"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Pokaż wszystkie widżety"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Szerokość %1$d, wysokość %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notatki"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodaj widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Kliknij, aby zmienić ustawienia widżetu"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Zmień ustawienia widżetu"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Wyszukaj aplikacje"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Zainstaluj"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Nie proponuj aplikacji"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Przypnij podpowiedź"</string>
+    <string name="bubble" msgid="3072951361014076670">"Dymek"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Instalowanie skrótów"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Pozwala aplikacji dodawać skróty bez interwencji użytkownika."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"Odczytuje ustawienia i skróty na ekranie głównym"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Funkcja wyłączona przez administratora"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Zezwalaj na obrót ekranu głównego"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Po obróceniu telefonu"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Kropki powiadomień"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Włączone"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Wyłączone"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instaluję aplikację <xliff:g id="NAME">%1$s</xliff:g>, postęp: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Pobieranie elementu <xliff:g id="NAME">%1$s</xliff:g>, ukończono: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> oczekuje na instalację"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacja <xliff:g id="NAME">%1$s</xliff:g> jest zarchiwizowana. Kliknij, aby ją pobrać i przywrócić."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacja <xliff:g id="NAME">%1$s</xliff:g> jest zarchiwizowana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"pobierz i przywróć"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Wymagana aktualizacja aplikacji"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacja z tą ikoną nie jest aktualizowana. Możesz zaktualizować ją ręcznie, aby ponownie uruchomić ten skrót, lub usunąć ikonę."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualizuj"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Wstrzymaj aplikacje służbowe"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Cofnij wstrzymywanie"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtruj"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Niepowodzenie: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Przestrzeń prywatna"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 14a3cf5..04bff1f 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets desativados no Modo de segurança"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"O atalho não está disponível"</string>
     <string name="home_screen" msgid="5629429142036709174">"Página inicial"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Defina a app <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como a app inicial predefinida nas Definições"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ecrã dividido"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informações da app para %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Definições de utilização para %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nova janela"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar par de apps"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Este par de apps não é suportado neste dispositivo"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"O par de apps não está disponível"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Toque sem soltar para mover um widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toque duas vezes sem soltar para mover um widget ou utilizar ações personalizadas."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Mais opções"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostrar todos os widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largura por %2$d de altura"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Tomar notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Adicionar"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Adicione o widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toque para alterar as definições do widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Alterar definições do widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pesquisar apps"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalar"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Não sugerir app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fixar previsão"</string>
+    <string name="bubble" msgid="3072951361014076670">"Balão"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalar atalhos"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permite a uma app adicionar atalhos sem a intervenção do utilizador."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ler definições e atalhos do ecrã Principal"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Desativada pelo gestor"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir rotação do ecrã principal"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Quando o telemóvel é rodado"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pontos de notificação"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ativados"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desativados"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"A instalar <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"A transferir o <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"A aguardar a instalação do <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"A app <xliff:g id="NAME">%1$s</xliff:g> está arquivada. Toque para transferir e restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"A app <xliff:g id="NAME">%1$s</xliff:g> está arquivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"transferir e restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Atualização da app necessária"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"A app deste ícone não está atualizada. Pode atualizar manualmente para reativar este atalho ou remover o ícone."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atualizar"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausar apps de trabalho"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Retomar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrar"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Falhou: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espaço privado"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 4c4b13b..20cc9fb 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets desativados no modo de segurança"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"O atalho não está disponível"</string>
     <string name="home_screen" msgid="5629429142036709174">"Início"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Definir <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como app de início padrão nas Configurações"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Tela dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informações do app %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configurações de uso de %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nova janela"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salvar par de apps"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Este Par de apps não está disponível no dispositivo"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"O Par de apps não está disponível"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Toque e pressione para mover um widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toque duas vezes e mantenha a tela pressionada para mover um widget ou usar ações personalizadas."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Mais opções"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostrar todos os widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largura por %2$d de altura"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Anotações"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Adicionar"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Adicionar o widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toque para mudar as configurações do widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Mudar as configurações do widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pesquisar apps"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalar"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Não sugerir esse app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fixar previsão"</string>
+    <string name="bubble" msgid="3072951361014076670">"Balão"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalar atalhos"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permite que um app adicione atalhos sem intervenção do usuário."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ler configurações e atalhos da tela inicial"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Desativado pelo administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir a rotação da tela inicial"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Quando o smartphone for girado"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pontos de notificação"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ativados"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desativado"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalando <xliff:g id="NAME">%1$s</xliff:g>. <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Fazendo download de <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Aguardando instalação de <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"O app <xliff:g id="NAME">%1$s</xliff:g> está arquivado. Toque para baixar e restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"O app <xliff:g id="NAME">%1$s</xliff:g> está arquivado."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"baixar e restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Atualização obrigatória do app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"O app desse ícone não está atualizado. Você pode remover o ícone ou atualizar o app manualmente para reativar esse atalho."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atualizar"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ok"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausar apps de trabalho"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ativar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrar"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Falha: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espaço privado"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 256fa60..4226920 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgeturile sunt dezactivate în modul de siguranță"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Comanda rapidă nu este disponibilă"</string>
     <string name="home_screen" msgid="5629429142036709174">"Pagina de pornire"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Setează <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> drept aplicație ecran de pornire prestabilită, în Setări"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ecran împărțit"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informații despre aplicație pentru %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Setări de utilizare pentru %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Fereastră nouă"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salvează perechea de aplicații"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Perechea de aplicații nu este acceptată pe acest dispozitiv"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Perechea de aplicații nu este disponibilă"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Atinge și ține apăsat pentru a muta un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Atinge de două ori și ține apăsat pentru a muta un widget sau folosește acțiuni personalizate."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Mai multe opțiuni"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Afișează toate widgeturile"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d lățime și %2$d înălțime"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widgetul <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Luare de notițe"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Adaugă"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Adaugă widgetul <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Atinge ca să schimbi setările pentru widgeturi"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifică setările pentru widgeturi"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Caută aplicații"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalează"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Nu sugera aplicația"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fixează predicția"</string>
+    <string name="bubble" msgid="3072951361014076670">"Balon"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalează comenzi rapide"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permite unei aplicații să adauge comenzi rapide fără intervenția utilizatorului."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"citește setările și comenzile rapide de pe ecranul de pornire"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Dezactivată de administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permite rotirea ecranului de pornire"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Când telefonul este rotit"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puncte de notificare"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activate"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Dezactivate"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> se instalează, <xliff:g id="PROGRESS">%2$s</xliff:g> finalizat"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> se descarcă (finalizat <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> așteaptă instalarea"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> s-a arhivat. Atinge pentru a descărca și restabili."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> s-a arhivat."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descarcă și restabilește"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Este necesară actualizarea aplicației"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplicația pentru această pictogramă nu este actualizată. Poți să actualizezi manual ca să reactivezi comanda rapidă sau să elimini pictograma."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizează"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Întrerupe aplicațiile pentru lucru"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Anulează întreruperea"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtru"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Eșuare: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Spațiu privat"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 402143d..2b827ef 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Виджеты отключены в безопасном режиме"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Ярлык недоступен"</string>
     <string name="home_screen" msgid="5629429142036709174">"Главный экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Установить \"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>\" в качестве приложения главного экрана по умолчанию в настройках"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Разделить экран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Сведения о приложении \"%1$s\""</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Настройки использования приложения \"%1$s\""</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Новое окно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Сохранить приложения"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Одновременно использовать эти два приложения на устройстве нельзя."</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Одновременное использование двух приложений недоступно"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Чтобы переместить виджет, нажмите на него и удерживайте"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Чтобы использовать специальные действия или перенести виджет, нажмите на него дважды и удерживайте."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Другие параметры"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Показать все виджеты"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d x %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ширина %1$d, высота %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Виджет \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
@@ -46,7 +48,7 @@
     <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Нажмите на виджет и удерживайте его, чтобы переместить в нужное место на главном экране."</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"Добавить на главный экран"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Виджет \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\" добавлен на главный экран"</string>
-    <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Подсказки"</string>
+    <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Рекомендованные"</string>
     <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Основное"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Новости и журналы"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Развлечения"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Создание заметок"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Добавить"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Добавить виджет \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Нажмите, чтобы изменить настройки виджета"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Изменить настройки виджета"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Поиск приложений"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Установить"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Не рекомендовать"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Закрепить рекомендацию"</string>
+    <string name="bubble" msgid="3072951361014076670">"Подсказка"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"Создание ярлыков"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Приложение сможет самостоятельно добавлять ярлыки."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"Доступ к данным о настройках и ярлыках на главном экране"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Функция отключена администратором"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Разрешить поворачивать главный экран"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"При повороте телефона"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Значки уведомлений"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Включены"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Отключены"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Установка приложения \"<xliff:g id="NAME">%1$s</xliff:g>\" (выполнено <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Скачивается \"<xliff:g id="NAME">%1$s</xliff:g>\" (<xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Ожидание установки \"<xliff:g id="NAME">%1$s</xliff:g>\""</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Приложение \"<xliff:g id="NAME">%1$s</xliff:g>\" находится в архиве. Нажмите, чтобы скачать его и восстановить"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Приложение \"<xliff:g id="NAME">%1$s</xliff:g>\" находится в архиве."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"скачать и восстановить"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Обновите приложение"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Эта версия приложения устарела. Обновите его вручную, чтобы снова пользоваться ярлыком, или удалите значок."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Обновить"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ОК"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Приостановить рабочие приложения"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Возобновить"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Фильтр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не удалось выполнить действие (<xliff:g id="WHAT">%1$s</xliff:g>)."</string>
     <string name="private_space_label" msgid="2359721649407947001">"Частное пространство"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 455f18b..646443e 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"සුරක්ෂිත ආකාරය තුළ විජටය අබල කරන ලදි"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"කෙටි මග ලබා ගත නොහැකිය"</string>
     <string name="home_screen" msgid="5629429142036709174">"මුල් පිටුව"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> සැකසීම් තුළ පෙරනිමි මුල් පිටුව යෙදුම ලෙස සකසන්න"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"බෙදුම් තිරය"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s සඳහා යෙදුම් තතු"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s සඳහා භාවිත සැකසීම්"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"නව කවුළුව"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"යෙදුම් යුගල සුරකින්න"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"මෙම යෙදුම් යුගලය මෙම උපාංගයෙහි සහාය නොදක්වයි"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"යෙදුම් යුගලයක් නොමැත"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"විජට් එකක් ගෙන යාමට ස්පර්ශ කර අල්ලා ගෙන සිටින්න."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"විජට් එකක් ගෙන යාමට හෝ අභිරුචි ක්‍රියා භාවිත කිරීමට දෙවරක් තට්ටු කර අල්ලා ගෙන සිටින්න."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"තවත් විකල්ප"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"සියලු ම විජට් පෙන්වන්න"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"පළල %1$d උස %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> විජට්ටුව"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"සටහන් කර ගැනීම"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"එක් කරන්න"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> විජට්ටුව එක් කරන්න"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"විජට් සැකසීම් වෙනස් කිරීමට තට්ටු කරන්න"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"විජට් සැකසීම් වෙනස් කරන්න"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"යෙදුම් සොයන්න"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ස්ථාපනය කරන්න"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"යෙදුම යෝජනා නොකරන්න"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"පුරෝකථනය අමුණන්න"</string>
+    <string name="bubble" msgid="3072951361014076670">"බුබුළ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"කෙටිමං ස්ථාපනය කරන්න"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"පරිශීලක මැදිහත්වීමෙන් තොරව කෙටිමං එක් කිරීමට යෙදුමකට අවසර දෙයි."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"මුල් පිටු සැකසීම් සහ කෙටි මං කියවන්න"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ඔබගේ පරිපාලක විසින් අබල කරන ලදී"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"මුල් තිරය කරකැවීමට ඉඩ දෙන්න"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"දුරකථනය කරකවන විට"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"දැනුම්දීම් තිත්"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ක්‍රියාත්මකයි"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ක්‍රියාවිරහිතයි"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ස්ථාපනය කරමින්, <xliff:g id="PROGRESS">%2$s</xliff:g> සම්පූර්ණයි"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> බාගත කරමින්, <xliff:g id="PROGRESS">%2$s</xliff:g> සම්පූර්ණයි"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ස්ථාපනය කිරීමට බලා සිටිමින්"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> සංරක්‍ෂිතයි. බා ගෙන ප්‍රතිසාධන කිරීමට තට්ටු කරන්න."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ලේඛනාරක්ෂණය කර ඇත."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"බාගත කර ප්‍රතිසාධනය කරන්න"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"යෙදුම් යාවත්කාලීනයක් අවශ්‍යයි"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"මෙම නිරූපකය සඳහා යෙදුම යාවත්කාලීන කර නැත. ඔබට මෙම කෙටි මඟ යළි සබල කිරීමට හෝ නිරූපකය ඉවත් කිරීමට හස්තීයව යාවත්කාලීන කළ හැකිය."</string>
     <string name="dialog_update" msgid="2178028071796141234">"යාවත්කාලීන කරන්න"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"තේරුණා"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"කාර්යාල යෙදුම් විරාම කරන්න"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"විරාම නොකරන්න"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"පෙරහන"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"අසාර්ථකයි: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"පෞද්ගලික ඉඩ"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 7771cf7..f9245cd 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Miniaplikácie sú v núdzovom režime zakázané"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Skratky nie sú k dispozícii"</string>
     <string name="home_screen" msgid="5629429142036709174">"Domov"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Nastaviť aplikáciu <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ako predvolenú vstupnú aplikáciu v Nastaveniach"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Rozdeliť obrazovku"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informácie o aplikácii pre %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nastavenia používania pre %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nové okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Uložiť pár aplikácií"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Tento pár aplikácií nie je v tomto zariadení podporovaný"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Pár aplikácií nie je k dispozícii"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Pridržaním presuňte miniaplikáciu."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvojitým klepnutím a pridržaním presuňte miniaplikáciu alebo použite vlastné akcie."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Ďalšie možnosti"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Zobrazovať všetky miniaplikácie"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"šírka %1$d, výška %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Miniaplikácia <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -57,16 +59,22 @@
     <string name="shortcuts_count" msgid="8471715556199592381">"{count,plural, =1{# odkaz}few{# odkazy}many{# shortcuts}other{# odkazov}}"</string>
     <string name="widgets_and_shortcuts_count" msgid="7209136747878365116">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
     <string name="widget_button_text" msgid="2880537293434387943">"Miniaplikácie"</string>
-    <string name="widgets_full_sheet_search_bar_hint" msgid="8484659090860596457">"Vyhľadajte"</string>
+    <string name="widgets_full_sheet_search_bar_hint" msgid="8484659090860596457">"Vyhľadávanie"</string>
     <string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"Vymazať text z vyhľadávacieho poľa"</string>
     <string name="no_widgets_available" msgid="4337693382501046170">"Miniaplikácie a odkazy nie sú k dispozícii"</string>
     <string name="no_search_results" msgid="3787956167293097509">"Nenašli sa žiadne miniaplikácie ani odkazy"</string>
     <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Osobné"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Práca"</string>
+    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Pracovné"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"Konverzácie"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Zapisovanie poznámok"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Pridať"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Pridať miniaplikáciu <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Klepnutím zmeňte nastavenia miniaplikácie"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Zmena nastavení miniaplikácie"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Hľadať aplikácie"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Inštalovať"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Nenavrhovať aplikáciu"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Pripnúť predpoveď"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bublina"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"inštalácia odkazov"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Povoľuje aplikácii pridať odkazy bez zásahu používateľa."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"čítanie nastavení a odkazov plochy"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Zakázané vaším správcom"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Povoliť otáčanie plochy"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Pri otočení telefónu"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bodky upozornení"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Zapnuté"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Vypnuté"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Inštaluje sa <xliff:g id="NAME">%1$s</xliff:g>. Dokončené: <xliff:g id="PROGRESS">%2$s</xliff:g>."</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Sťahuje sa aplikácia <xliff:g id="NAME">%1$s</xliff:g>. Stiahnuté: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Aplikácia <xliff:g id="NAME">%1$s</xliff:g> čaká na inštaláciu"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikácia <xliff:g id="NAME">%1$s</xliff:g> je archivovaná. Klepnutím ju stiahnite a obnovte."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikácia <xliff:g id="NAME">%1$s</xliff:g> je archivovaná."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"stiahnuť a obnoviť"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Vyžaduje sa aktualizácia aplikácie"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikácia, ktorú zastupuje táto ikona, nie je aktualizovaná. Môžete ju ručne aktualizovať, aby odkaz znova fungoval, prípadne môžete ikonu odstrániť."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualizovať"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Dobre"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pozastaviť pracovné aplikácie"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Zrušiť pozastavenie"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrujte"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Zlyhalo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Súkromný priestor"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 2e75b17..e8d8702 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Pripomočki so onemogočeni v varnem načinu"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Bližnjica ni na voljo"</string>
     <string name="home_screen" msgid="5629429142036709174">"Začetni zaslon"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Nastavitev zaganjalnika <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kot privzete aplikacije za začetni zaslon v nastavitvah"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Razdeljen zaslon"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Podatki o aplikaciji za: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nastavitve uporabe za »%1$s«"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novo okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Shrani par aplikacij"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ta par aplikacij ni podprt v tej napravi"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacij ni na voljo"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Pridržite pripomoček, da ga premaknete."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvakrat se dotaknite pripomočka in ga pridržite, da ga premaknete, ali pa uporabite dejanja po meri."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Več možnosti"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Pokaži vse pripomočke"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Širina %1$d, višina %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Pripomoček <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ustvarjanje zapiskov"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodajanje pripomočka »<xliff:g id="WIDGET_NAME">%1$s</xliff:g>«"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dotaknite se, če želite spremeniti nastavitve pripomočka."</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Spreminjanje nastavitev pripomočka"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Iskanje programov"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Namesti"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne predlagaj"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Predvidevanje pripenjanja"</string>
+    <string name="bubble" msgid="3072951361014076670">"Mehurček"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"namestitev bližnjic"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Aplikaciji dovoli dodajanje bližnjic brez posredovanja uporabnika."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"branje nastavitev in bližnjic na začetnem zaslonu"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Onemogočil skrbnik."</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dovoli sukanje začetnega zaslona"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Ko se telefon zasuka"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Obvestilne pike"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Vklopljeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Izklopljeno"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> se namešča, dokončano: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Prenašanje aplikacije <xliff:g id="NAME">%1$s</xliff:g>; preneseno <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> čaka na namestitev"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana. Dotaknite se za prenos in obnovitev."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"prenos in obnovitev"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Zahtevana je posodobitev aplikacije"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija za to ikono ni posodobljena. Lahko jo ročno posodobite, da znova omogočite to bližnjico, ali pa odstranite ikono."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Posodobi"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"V redu"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Začasno zaustavi delovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Znova aktiviraj"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtriranje"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Ni uspelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Zasebni prostor"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index a00f11a..1e5420a 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Miniaplikacionet janë të çaktivizuara në modalitetin e sigurt"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shkurtorja nuk është e disponueshme"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ekrani bazë"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Cakto <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> si aplikacionin e parazgjedhur të ekranit bazë te \"Cilësimet\""</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ekrani i ndarë"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacioni i aplikacionit për %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Cilësimet e përdorimit për \"%1$s\""</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Dritare e re"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Ruaj çiftin e aplikacioneve"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ky çift aplikacionesh nuk mbështetet në këtë pajisje"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Çifti i aplikacioneve nuk ofrohet"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Prek dhe mbaj shtypur një miniaplikacion për ta zhvendosur."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Trokit dy herë dhe mbaje shtypur një miniapliikacion për ta zhvendosur atë ose për të përdorur veprimet e personalizuara."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Opsione të tjera"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Shfaq të gjitha miniapl."</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d i gjerë me %2$d i lartë"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> miniaplikacion"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Mbajtja e shënimeve"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Shto"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Shto miniaplikacionin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Trokit për të ndryshuar cilësimet e miniaplikacionit"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Ndrysho cilësimet e miniaplikacionit"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Kërko për aplikacione"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Instalo"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Mos sugjero aplikacion"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Gozhdo parashikimin"</string>
+    <string name="bubble" msgid="3072951361014076670">"Flluskë"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"instalimi i shkurtoreve"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Lejon një aplikacion të shtojë shkurtore pa ndërhyrjen e përdoruesit."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"lexo cilësimet dhe shkurtoret e ekranit bazë"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Çaktivizuar nga administratori"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Lejo rrotullimin e ekranit bazë"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kur telefoni rrotullohet"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pikat e njoftimeve"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktiv"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Joaktiv"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> po instalohet, <xliff:g id="PROGRESS">%2$s</xliff:g> i përfunduar"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> po shkarkohet, <xliff:g id="PROGRESS">%2$s</xliff:g> të përfunduara"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> po pret të instalohet"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> është arkivuar. Trokit për ta shkarkuar dhe restauruar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> është arkivuar."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"shkarko dhe restauro"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kërkohet përditësimi i aplikacionit"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacioni për këtë ikonë nuk është përditësuar. Mund ta përditësosh manualisht për të riaktivizuar këtë shkurtore ose hiq ikonën."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Përditëso"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"E kuptova"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Vendos në pauzë aplikacionet e punës"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Hiq nga pauza"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtro"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Dështoi: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Hapësira private"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 97d3ff4..c5b7bd0 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Виџети су онемогућени у Безбедном режиму"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Пречица није доступна"</string>
     <string name="home_screen" msgid="5629429142036709174">"Почетни екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Подесите <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> као подразумевану почетну апликацију у Подешавањима"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Подељени екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Информације о апликацији за: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Подешавања потрошње за %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нови прозор"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Сачувај пар апликација"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Овај пар апликација није подржан на овом уређају"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Пар апликација није доступан"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Додирните и задржите ради померања виџета."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Двапут додирните и задржите да бисте померали виџет или користите прилагођене радње."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Још опција"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Прикажи све виџете"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d×%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"ширина од %1$d и висина од %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виџет"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Прављење бележака"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Додај"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Додајте виџет <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Додирните да бисте променили подешавања виџета"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Промените подешавања виџета"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Претражите апликације"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Инсталирај"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Не предлажи апликацију"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Закачи предвиђање"</string>
+    <string name="bubble" msgid="3072951361014076670">"Облачић"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"инсталирање пречица"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Дозвољава апликацији да додаје пречице без интервенције корисника."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"читање подешавања и пречица на почетном екрану"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Администратор је онемогућио"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дозволи ротацију почетног екрана"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Када се телефон ротира"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Тачке за обавештења"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Укључено"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Искључено"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> се инсталира, <xliff:g id="PROGRESS">%2$s</xliff:g> готово"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> се преузима, завршено је <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> чека на инсталирање"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Апликација <xliff:g id="NAME">%1$s</xliff:g> је архивирана. Додирните да бисте је преузели и вратили."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Апликација <xliff:g id="NAME">%1$s</xliff:g> је архивирана."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"преузмите и вратите"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Треба да ажурирате апликацију"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Апликација за ову икону није ажурирана. Можете да је ручно ажурирате да бисте поново омогућили ову пречицу или уклоните икону."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ажурирај"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Важи"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Паузирај пословне апликације"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Поново активирај"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Филтер"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Није успело: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Приватни простор"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 8a8524e..9ebf366 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets är inaktiverade i felsäkert läge"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Genvägen är inte tillgänglig"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startskärm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ställ in <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> som standardstartskärmsapp i Inställningar"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Delad skärm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Appinformation för %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Användningsinställningar för %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nytt fönster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Spara app-par"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"De här apparna som ska användas tillsammans stöds inte på den här enheten"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App-paret är inte tillgängligt"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tryck länge för att flytta en widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Tryck snabbt två gånger och håll kvar för att flytta en widget eller använda anpassade åtgärder."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Fler alternativ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Visa alla widgetar"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d bred gånger %2$d hög"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget för <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -61,12 +63,18 @@
     <string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"Rensa texten från sökrutan"</string>
     <string name="no_widgets_available" msgid="4337693382501046170">"Widgetar och genvägar är inte tillgängliga"</string>
     <string name="no_search_results" msgid="3787956167293097509">"Inga widgetar eller genvägar hittades"</string>
-    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Privata"</string>
+    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Privat"</string>
     <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Arbete"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"Konversationer"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Anteckna"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Lägg till"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Lägg till widgeten <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tryck för att ändra inställningarna för widgeten"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Ändra inställningarna för widgeten"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Sök efter appar"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Installera"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Föreslå inte app"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Fäst förslag"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubbla"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installera genvägar"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Tillåter att en app lägger till genvägar utan åtgärd från användaren."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"läsa inställningar och genvägar på startskärmen"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Inaktiverat av administratören"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Tillåt rotering av startskärmen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"När telefonen vrids"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Aviseringsprickar"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"På"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Av"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeras. <xliff:g id="PROGRESS">%2$s</xliff:g> har slutförts"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> laddas ned, <xliff:g id="PROGRESS">%2$s</xliff:g> klart"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> väntar på installation"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> har arkiverats. Tryck för att ladda ner och återställa."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> har arkiverats."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ladda ned och återställ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Du måste uppdatera appen"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Appen för den här ikonen har inte uppdaterats. Du kan uppdatera den manuellt för att återaktivera genvägen eller ta bort ikonen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Uppdatera"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausa jobbappar"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Återuppta"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Misslyckades: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privat rum"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 64f6296..a7725f3 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Wijeti zimezimwa katika hali ya Usalama"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Hakuna njia ya mkato"</string>
     <string name="home_screen" msgid="5629429142036709174">"Skrini ya kwanza"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Weka <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> iwe programu chaguomsingi ya mwanzo kwenye Mipangilio"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Gawa skrini"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Maelezo ya programu ya %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Mipangilio ya matumizi ya %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Dirisha Jipya"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Hifadhi jozi ya programu"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Jozi hii ya programu haitumiki kwenye kifaa hiki"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Kipengele cha jozi ya programu hakipatikani"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Gusa na ushikilie ili usogeze wijeti."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Gusa mara mbili na ushikilie ili usogeze wijeti au utumie vitendo maalum."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Chaguo zaidi"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Onyesha wijeti zote"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Upana wa %1$d na kimo cha %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Wijeti ya <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Kuandika madokezo"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Weka"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Weka wijeti ya <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Gusa ili ubadilishe mipangilio ya wijeti"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Badilisha mipangilio ya wijeti"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Tafuta programu"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Sakinisha"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Isipendekeze programu"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Bandika Utabiri"</string>
+    <string name="bubble" msgid="3072951361014076670">"Kiputo"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"kuweka njia za mkato"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Huruhusu programu kuongeza njia za mkato bila mtumiaji kuingilia kati."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"kusoma mipangilio ya skrini ya kwanza na njia za mkato"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Imezimwa na msimamizi wako"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Ruhusu kipengele cha kuzungusha skrini ya kwanza"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Simu inapozungushwa"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Vitone vya arifa"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Imewashwa"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Imezimwa"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Inasakinisha <xliff:g id="NAME">%1$s</xliff:g>, imekamilika <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> inapakuliwa, <xliff:g id="PROGRESS">%2$s</xliff:g> imekamilika"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> inasubiri kusakinisha"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> imewekwa kwenye kumbukumbu. Gusa ili upakue na urejeshe."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> imewekwa kwenye kumbukumbu."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"pakua na urejeshe"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Unahitaji kusasisha programu"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Programu ya aikoni hii haijasasishwa. Unaweza kusasisha mwenyewe ili uruhusu upya njia hii ya mkato au uondoe aikoni."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Sasisha"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Nimeelewa"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Simamisha programu za kazini"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Acha kusimamisha"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Kichujio"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Hitilafu: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Nafasi ya faragha"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 189f4ce..295367d 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"பாதுகாப்புப் பயன்முறையில் விட்ஜெட்கள் முடக்கப்பட்டுள்ளன"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ஷார்ட்கட் இல்லை"</string>
     <string name="home_screen" msgid="5629429142036709174">"முகப்பு"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"அமைப்புகளில் <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> என்பதை இயல்பு முகப்பு ஆப்ஸாக அமையுங்கள்"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"திரைப் பிரிப்பு"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$sக்கான ஆப்ஸ் தகவல்கள்"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sக்கான உபயோக அமைப்புகள்"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"புதிய சாளரம்"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ஆப்ஸ் ஜோடியைச் சேமி"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"இந்தச் சாதனத்தில் இந்த ஆப்ஸ் ஜோடி ஆதரிக்கப்படவில்லை"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ஆப்ஸ் ஜோடி கிடைக்கவில்லை"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"விட்ஜெட்டை நகர்த்தத் தொட்டுப் பிடிக்கவும்."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"விட்ஜெட்டை நகர்த்த இருமுறை தட்டிப் பிடிக்கவும் அல்லது பிரத்தியேகச் செயல்களைப் பயன்படுத்தவும்."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"கூடுதல் விருப்பங்கள்"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"விட்ஜெட்டுகளைக் காட்டு"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d அகலத்திற்கு %2$d உயரம்"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> விட்ஜெட்"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"குறிப்பெடுத்தல்"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"சேர்"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> விட்ஜெட்டைச் சேர்க்கும்"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"விட்ஜெட் அமைப்புகளை மாற்றத் தட்டவும்"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"விட்ஜெட் அமைப்புகளை மாற்றும்"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ஆப்ஸில் தேடுக"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"நிறுவு"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"பரிந்துரைக்காதே"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"கணிக்கப்பட்டதைப் பின் செய்"</string>
+    <string name="bubble" msgid="3072951361014076670">"குமிழ்"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"குறுக்குவழிகளை நிறுவுதல்"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"பயனரின் அனுமதி இல்லாமல் குறுக்குவழிகளைச் சேர்க்கப் ஆப்ஸை அனுமதிக்கிறது."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"முகப்புத் திரையின் அமைப்புகளையும் ஷார்ட்கட்களையும் படித்தல்"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"உங்கள் நிர்வாகி முடக்கியுள்ளார்"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"முகப்புத் திரை சுழற்சியை அனுமதித்தல்"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"மொபைலைச் சுழற்றும் போது"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"அறிவிப்புப் புள்ளிகள்"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ஆன்"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ஆஃப்"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> நிறுவப்படுகிறது, <xliff:g id="PROGRESS">%2$s</xliff:g> முடிந்தது"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g>ஐப் பதிவிறக்குகிறது, <xliff:g id="PROGRESS">%2$s</xliff:g> முடிந்தது"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g>ஐ நிறுவுவதற்காகக் காத்திருக்கிறது"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> காப்பிடப்பட்டுள்ளது. அதைப் பதிவிறக்கி மீட்டெடுக்க தட்டுங்கள்."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> காப்பிடப்பட்டுள்ளது."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"பதிவிறக்கி மீட்டெடுக்கும்"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ஆப்ஸைப் புதுப்பியுங்கள்"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"இந்த ஐகானுக்கான ஆப்ஸ் புதுப்பிக்கப்படவில்லை. இந்த ஷார்ட்கட்டை மீண்டும் இயக்கவோ ஐகானை அகற்றவோ நீங்களாகவே புதுப்பிக்கலாம்."</string>
     <string name="dialog_update" msgid="2178028071796141234">"புதுப்பி"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"சரி"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"பணி ஆப்ஸை இடைநிறுத்து"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"மீண்டும் இயக்கு"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"வடிப்பான்"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"தோல்வி: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"தனிப்பட்ட சேமிப்பிடம்"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index d789baa..5859665 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"సురక్షిత మోడ్‌లో విడ్జెట్‌లు నిలిపివేయబడ్డాయి"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"షార్ట్‌కట్ అందుబాటులో లేదు"</string>
     <string name="home_screen" msgid="5629429142036709174">"మొదటి ట్యాబ్"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"సెట్టింగ్‌లలో <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>‌ను ఆటోమేటిక్ సెట్టింగ్ మొదటి స్క్రీన్ యాప్‌గా సెట్ చేయండి"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"స్ప్లిట్ స్క్రీన్"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s కోసం యాప్ సమాచారం"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sకు సంబంధించిన వినియోగ సెట్టింగ్‌లు"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"కొత్త విండో"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"యాప్ పెయిర్‌ను సేవ్ చేయండి"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ఈ పరికరంలో ఈ యాప్ పెయిర్ సపోర్ట్ చేయదు"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"యాప్ పెయిర్ అందుబాటులో లేదు"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"విడ్జెట్‌ను తరలించడానికి తాకి &amp; నొక్కి ఉంచండి."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"విడ్జెట్‌ను తరలించడానికి లేదా అనుకూల చర్యలను ఉపయోగించడానికి రెండుసార్లు నొక్కండి &amp; హోల్డ్ చేయి."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"మరిన్ని ఆప్షన్‌లు"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"అన్ని విడ్జెట్‌లను చూడండి"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d వెడల్పు X %2$d ఎత్తు"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"నోట్-టేకింగ్"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"జోడించండి"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్‌ను జోడించండి"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"విడ్జెట్ సెట్టింగ్‌లను మార్చడానికి ట్యాప్ చేయండి"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"విడ్జెట్ సెట్టింగ్‌లను మార్చండి"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"యాప్‌ల కోసం సెర్చ్ చేయండి"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ఇన్‌స్టాల్ చేయండి"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"యాప్ సూచించకు"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"సూచనను పిన్ చేయండి"</string>
+    <string name="bubble" msgid="3072951361014076670">"బబుల్"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"షార్ట్‌కట్‌లను ఇన్‌స్టాల్ చేయడం"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"వినియోగదారు ప్రమేయం లేకుండా షార్ట్‌కట్‌లను జోడించడానికి యాప్‌ను అనుమతిస్తుంది."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"హోమ్ సెట్టింగ్‌లు, షార్ట్‌కట్‌లను చదవండి"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"మీ నిర్వాహకులు నిలిపివేసారు"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"మొదటి స్క్రీన్ రొటేషన్‌ను అనుమతించండి"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ఫోన్‌‌ను తిప్పినప్పుడు"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"నోటిఫికేషన్ డాట్‌లు"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ఆన్"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ఆఫ్"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g>‌ను ఇన్‌స్టాల్ చేయడం, <xliff:g id="PROGRESS">%2$s</xliff:g> పూర్తయింది"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> డౌన్‌లోడ్ అవుతోంది, <xliff:g id="PROGRESS">%2$s</xliff:g> పూర్తయింది"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ఇన్‌స్టాల్ కావడానికి వేచి ఉంది"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ఆర్కైవ్ చేయబడింది. డౌన్‌లోడ్ చేయడానికి, రీస్టోర్ చేయడానికి ట్యాప్ చేయండి."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ఆర్కైవ్ చేయబడింది."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"డౌన్‌లోడ్ చేసి, రీస్టోర్ చేయండి"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"యాప్‌ను అప్‌డేట్ చేయడం అవసరం"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ఈ చిహ్నం కోసం యాప్ అప్‌డేట్ చేయబడలేదు. మీరు ఈ షార్ట్‌కట్‌ను మళ్లీ ఎనేబుల్ చేయడానికి మాన్యువల్‌గా అప్‌డేట్ చేయవచ్చు లేదా చిహ్నాన్ని తీసివేయవచ్చు."</string>
     <string name="dialog_update" msgid="2178028071796141234">"అప్‌డేట్ చేయండి"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"అర్థమైంది"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"వర్క్ యాప్‌లను పాజ్ చేయండి"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"పాజ్ నుండి తీసివేయండి"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ఫిల్టర్ చేయి"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"విఫలమైంది: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ప్రైవేట్ స్పేస్"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index f89638d..fd2ba21 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"มีการปิดใช้งานวิดเจ็ตในเซฟโหมด"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ทางลัดไม่พร้อมใช้งาน"</string>
     <string name="home_screen" msgid="5629429142036709174">"หน้าแรก"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ตั้ง <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> เป็นแอปหน้าแรกเริ่มต้นในการตั้งค่า"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"แยกหน้าจอ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"ข้อมูลแอปสำหรับ %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"การตั้งค่าการใช้งานสำหรับ %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"หน้าต่างใหม่"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"บันทึกคู่แอป"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ไม่รองรับคู่แอปนี้ในอุปกรณ์เครื่องนี้"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"การจับคู่อุปกรณ์ไม่พร้อมให้บริการ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"แตะค้างไว้เพื่อย้ายวิดเจ็ต"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"แตะสองครั้งค้างไว้เพื่อย้ายวิดเจ็ตหรือใช้การดำเนินการที่กำหนดเอง"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ตัวเลือกเพิ่มเติม"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"แสดงวิดเจ็ตทั้งหมด"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"กว้าง %1$d x สูง %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"วิดเจ็ต <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"การจดบันทึก"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"เพิ่ม"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"เพิ่มวิดเจ็ต <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"แตะเพื่อเปลี่ยนการตั้งค่าวิดเจ็ต"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"เปลี่ยนการตั้งค่าวิดเจ็ต"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ค้นหาแอป"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"ติดตั้ง"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ไม่ต้องแนะนำแอป"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"ปักหมุดแอปที่คาดการณ์ไว้"</string>
+    <string name="bubble" msgid="3072951361014076670">"บับเบิล"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"ติดตั้งทางลัด"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"อนุญาตให้แอปเพิ่มทางลัดโดยไม่ต้องให้ผู้ใช้จัดการ"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"อ่านการตั้งค่าและทางลัดในหน้าแรก"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ปิดใช้โดยผู้ดูแลระบบ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"อนุญาตให้หมุนหน้าจอหลัก"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"เมื่อหมุนโทรศัพท์"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"เครื่องหมายจุดแสดงการแจ้งเตือน"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"เปิด"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ปิด"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"กำลังติดตั้ง <xliff:g id="NAME">%1$s</xliff:g> เสร็จแล้ว <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"กำลังดาวน์โหลด <xliff:g id="NAME">%1$s</xliff:g> เสร็จแล้ว <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> กำลังรอติดตั้ง"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"เก็บถาวร <xliff:g id="NAME">%1$s</xliff:g> แล้ว แตะเพื่อดาวน์โหลดและกู้คืน"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"เก็บถาวร <xliff:g id="NAME">%1$s</xliff:g> แล้ว"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ดาวน์โหลดและกู้คืน"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ต้องอัปเดตแอป"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"แอปสำหรับไอคอนนี้ยังไม่ได้อัปเดต คุณอัปเดตด้วยตนเองได้โดยเปิดใช้ทางลัดนี้อีกครั้งหรือนำไอคอนออก"</string>
     <string name="dialog_update" msgid="2178028071796141234">"อัปเดต"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"รับทราบ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"หยุดแอปงานชั่วคราว"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ยกเลิกการหยุดชั่วคราว"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ตัวกรอง"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ไม่สำเร็จ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"พื้นที่ส่วนตัว"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 8754a12..69f88e1 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Naka-disable ang mga widget sa Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Hindi available ang shortcut"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Itakda ang <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> bilang default na home app sa Mga Setting"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Impormasyon ng app para sa %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Mga setting ng paggamit para sa %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Bagong Window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"I-save ang app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Hindi sinusuportahan sa device na ito ang pares ng app na ito"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Hindi available ang pares ng app"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Pindutin nang matagal para ilipat ang widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"I-double tap at pindutin nang matagal para ilipat ang widget o gumamit ng mga custom na pagkilos."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Higit pang opsyon"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Ipakita lahat ng widget"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ang lapad at %2$d ang taas"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pagtatala"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Idagdag"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Idagdag ang widget na <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"I-tap para baguhin ang mga setting ng widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Baguhin ang mga setting ng widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Maghanap ng mga app"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"I-install"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Huwag magmungkahi"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"I-pin ang Hula"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bubble"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"i-install ang mga shortcut"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Pinapayagan ang isang app na magdagdag ng mga shortcut nang walang panghihimasok ng user."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"basahin ang mga setting at shortcut ng home"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Na-disable ng iyong admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Payagan ang pag-rotate ng home screen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kailan maro-rotate ang telepono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Mga notification dot"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Naka-on"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Naka-off"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Ini-install ang <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> kumpleto"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Dina-download na ang <xliff:g id="NAME">%1$s</xliff:g>, tapos na ang <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Hinihintay nang mag-install ang <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Naka-archive ang <xliff:g id="NAME">%1$s</xliff:g>. I-tap para i-download at i-restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Naka-archive ang <xliff:g id="NAME">%1$s</xliff:g>."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"i-download at i-restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kinakailangang i-update ang app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Hindi updated ang app para sa icon na ito. Puwede kang manual na mag-update para ma-enable ulit ang shortcut na ito, o alisin ang icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"I-update"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"I-pause ang mga app para sa trabaho"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"I-unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Hindi nagawa: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Pribadong space"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 761ce56..e61b670 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Güvenli modda widget\'lar devre dışı"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Kısayol kullanılamıyor"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ana ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> başlatıcısını Ayarlar\'da varsayılan ana ekran uygulaması olarak ayarlayın"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Bölünmüş ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s uygulama bilgileri"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ile ilgili kullanım ayarları"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yeni Pencere"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Uygulama çiftini kaydedin"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Bu uygulama çifti bu cihazda desteklenmiyor"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Uygulama çifti kullanılamıyor"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Widget\'ı taşımak için dokunup basılı tutun."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Widget\'ı taşımak veya özel işlemleri kullanmak için iki kez dokunup basılı tutun."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Diğer seçenekler"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Tüm widget\'ları göster"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"genişlik: %1$d, yükseklik: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget\'ı"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Not alma"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ekle"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget\'ı ekle"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Widget ayarlarını değiştirmek için dokunun"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Widget ayarlarını değiştir"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Uygulamalarda ara"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Yükle"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Uygulamayı önerme"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Tahmini Sabitle"</string>
+    <string name="bubble" msgid="3072951361014076670">"Balon"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"kısayolları yükle"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Uygulamaya, kullanıcı müdahalesi olmadan kısayol ekleme izni verir."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ana ekran ayarlarını ve kısayollarını oku"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Yöneticiniz tarafından devre dışı bırakıldı"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Ana ekranı döndürmeye izin ver"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefon döndürüldüğünde"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bildirim noktaları"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Açık"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Kapalı"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> yükleniyor, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlandı"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> indiriliyor, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlandı"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> uygulaması yüklenmek için bekliyor"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> arşivlendi. İndirip geri yüklemek için dokunun."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> arşivlendi."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"indir ve geri yükle"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Uygulama güncellemesi gerekli"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Bu simgenin uygulaması güncellenmemiş. Simgeyi kaldırabilir ya da uygulamayı manuel olarak güncelleyerek bu kısayolu yeniden etkinleştirebilirsiniz."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Güncelle"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Anladım"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"İş uygulamalarını duraklat"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Devam ettir"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtre"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Başarısız: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Gizli alan"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 2c85d5a..fd293e3 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"У безпечному режимі віджети вимкнено"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Ярлик недоступний"</string>
     <string name="home_screen" msgid="5629429142036709174">"Головний екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Зробити <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> додатком головного екрана за умовчанням у налаштуваннях"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Розділити екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Інформація про додаток для %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Параметри використання (%1$s)"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нове вікно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Зберегти пару додатків"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ці два додатки не можна одночасно використовувати на цьому пристрої"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Одночасне використання двох додатків недоступне"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Натисніть і втримуйте, щоб перемістити віджет."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Двічі натисніть і втримуйте віджет, щоб перемістити його або виконати інші дії."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Інші опції"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Показати всі віджети"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ширина – %1$d, висота – %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Віджет <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Створення нотаток"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Додати"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Додати віджет \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Натисніть, щоб змінити налаштування віджета"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Змінити налаштування віджета"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Пошук додатків"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Установити"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Не пропонувати додаток"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Закріпити передбачений додаток"</string>
+    <string name="bubble" msgid="3072951361014076670">"Повідомлення"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"створення ярликів"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Дозволяє програмі самостійно додавати ярлики."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"читати налаштування та ярлики головного екрана"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Вимкнув адміністратор"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дозволити обертання головного екрана"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Коли телефон обертається"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Значки сповіщень"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Увімкнено"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Вимкнено"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> встановлюється, виконано <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> завантажується, <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> очікує на завантаження"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Додаток <xliff:g id="NAME">%1$s</xliff:g> заархівовано. Натисніть, щоб завантажити й відновити."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Додаток <xliff:g id="NAME">%1$s</xliff:g> заархівовано."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"завантажити й відновити"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Потрібно оновити додаток"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Додаток для цього значка не оновлено. Ви можете оновити його вручну, щоб знову ввімкнути цю швидку команду, або можете вилучити значок."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Оновити"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Зрозуміло"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Призупинити робочі додатки"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Відновити"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Фільтр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не вдалося <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Приватний простір"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 09f4304..911a2d5 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ویجیٹس کو محفوظ وضع میں غیر فعال کر دیا گیا"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"شارٹ کٹ دستیاب نہیں ہے"</string>
     <string name="home_screen" msgid="5629429142036709174">"ہوم"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ترتیبات میں <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> کو بطور ڈیفالٹ ہوم ایپ سیٹ کریں"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"اسپلٹ اسکرین"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏%1$s کے لیے ایپ کی معلومات"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏%1$s کیلئے استعمال کی ترتیبات"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"نئی ونڈو"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ایپس کے جوڑے کو محفوظ کریں"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ایپس کا یہ جوڑا اس آلے پر تعاون یافتہ نہیں ہے"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ایپ کا جوڑا دستیاب نہیں ہے"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ویجیٹ منتقل کرنے کے لیے ٹچ کریں اور پکڑ کر رکھیں۔"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ویجیٹ کو منتقل کرنے یا حسب ضرورت کارروائیاں استعمال کرنے کے لیے دوبار تھپتھپائیں اور پکڑ کر رکھیں۔"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"مزید اختیارات"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"سبھی ویجیٹس دکھائیں"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏%1$d چوڑا اور ‎%2$d اونچا"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ویجیٹ"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"نوٹ لکھنا"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"شامل کریں"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ویجیٹ شامل کریں"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ویجیٹ ترتیبات تبدیل کرنے کے لیے تھپتھپائیں"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ویجیٹ ترتیبات تبدیل کریں"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ایپس تلاش کریں"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"انسٹال کریں"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"ایپ تجویز نہ کریں"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"پیشگوئی پن کریں"</string>
+    <string name="bubble" msgid="3072951361014076670">"بلبلہ"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"شارٹ کٹس انسٹال کریں"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"کسی ایپ کو صارف کی مداخلت کے بغیر شارٹ کٹس شامل کرنے کی اجازت دیتا ہے۔"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"ہوم ترتیبات اور شارٹ کٹس کو پڑھیں"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"آپ کے منتظم کی طرف سے غیر فعال کر دیا گیا"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ہوم اسکرین گھمانے کی اجازت دیں"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"جب فون گھمایا جاتا ہے"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"اطلاعاتی ڈاٹس"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"آن ہے"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"آف ہے"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> انسٹال کی جا رہی ہے، <xliff:g id="PROGRESS">%2$s</xliff:g> مکمل ہو گئی"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> ڈاؤن لوڈ ہو رہا ہے، <xliff:g id="PROGRESS">%2$s</xliff:g> مکمل ہو گیا"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> انسٹال ہونے کا انتظار کر رہی ہے"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> کو آرکائیو کر لیا گیا ہے۔ ڈاؤن لوڈ اور بحال کرنے کیلئے تھپتھپائیں۔"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"‫<xliff:g id="NAME">%1$s</xliff:g> کو آرکائیو کر لیا گیا ہے۔"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ڈاؤن لوڈ کریں اور بحال کریں"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ایپ کی اپ ڈیٹ درکار ہے"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"اس آئیکن کیلئے ایپ کو اپ ڈیٹ نہیں کیا گیا ہے۔ آپ اس شارٹ کٹ کو دوبارہ فعال کرنے کے لیے دستی طور پر اپ ڈیٹ کر سکتے ہیں، یا آئیکن کو ہٹا سکتے ہیں۔"</string>
     <string name="dialog_update" msgid="2178028071796141234">"اپ ڈیٹ کریں"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"سمجھ آ گئی"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ورک ایپس موقوف کریں"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"چلائیں"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"فلٹر"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ناکام ہو گيا: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"نجی اسپیس"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index d8dea69..aacb35f 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Xavfsiz rejimda vidjetlar o‘chirib qo‘yilgan"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Tezkor tugmadan foydalanib bo‘lmaydi"</string>
     <string name="home_screen" msgid="5629429142036709174">"Bosh ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Sozlamalar orqali <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ilovasini birlamchi bosh ekran ilovasi sifatida belgilash"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ekranni ikkiga ajratish"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ilovasi axboroti"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s uchun sarf sozlamalari"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yangi oyna"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Ilova juftini saqlash"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Bu ilova jufti ushbu qurilmada ishlamaydi"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Ikkita ilovadan bir vaqtda foydalanish mumkin emas"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Vidjetni bosib turgan holatda suring."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ikki marta bosib va bosib turgan holatda vidjetni tanlang yoki maxsus amaldan foydalaning."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Yana"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Barcha vidjetlar"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Eni %1$d, bo‘yi %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ta vidjet"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Qayd olish"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Chiqarish"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidjetini chiqarish"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Vidjet sozlamalarini oʻzgartirish uchun bosing"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Vidjet sozlamalarini oʻzgartirish"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Ilovalarni qidirish"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"O‘rnatish"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Tavsiya qilinmasin"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Tavsiyani mahkamlash"</string>
+    <string name="bubble" msgid="3072951361014076670">"Pufaklar"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"yorliqlar yaratish"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Ilovalarga foydalanuvchidan so‘ramasdan yorliqlar qo‘shishga ruxsat beradi."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"Bosh ekrandagi sozlamalar va yorliqlarni koʻrish"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administrator tomonidan o‘chirilgan"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Bosh ekranni burishga ruxsat"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefon burilganda"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bildirishnoma belgilari"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Yoniq"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Oʻchiq"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> oʻrnatlmoqda, <xliff:g id="PROGRESS">%2$s</xliff:g> yakunlandi"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> yuklab olinmoqda, <xliff:g id="PROGRESS">%2$s</xliff:g> bajarildi"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ilovasi o‘rnatilishi kutilmoqda"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> arxivlangan. Yuklab olish va tiklash uchun bosing."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> arxivlangan."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"yuklab olish va tiklash"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Ilovani yangilash zarur"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Bu belgi uchun ilova yangilanmagan. Ushbu yorliqni qayta yoqish uchun oddiy usulda yangilashingiz yoki belgini olib tashlashingiz mumkin."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Yangilash"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Ishga oid ilovalarni pauza qilish"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Pauzadan chiqarish"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Saralash"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Xato: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Shaxsiy xona"</string>
diff --git a/res/values-v30/styles.xml b/res/values-v30/styles.xml
index ec5c113..a5c57c8 100644
--- a/res/values-v30/styles.xml
+++ b/res/values-v30/styles.xml
@@ -29,5 +29,6 @@
         <item name="android:windowLayoutInDisplayCutoutMode">always</item>
         <item name="android:enforceStatusBarContrast">false</item>
         <item name="android:enforceNavigationBarContrast">false</item>
+        <item name="materialColorOnPrimaryFixed">#FFFFFFFF</item>
     </style>
 </resources>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index 7270366..d74e308 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -104,45 +104,7 @@
     <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_secondary_fixed_variant">@android:color/system_accent2_700</color>
-    <color name="material_color_on_tertiary_fixed_variant">@android:color/system_accent3_700</color>
-    <color name="material_color_on_primary_fixed_variant">@android:color/system_accent1_700</color>
-    <color name="material_color_on_secondary_container">@android:color/system_accent2_900</color>
-    <color name="material_color_on_tertiary_container">@android:color/system_accent3_900</color>
-    <color name="material_color_on_primary_container">@android:color/system_accent1_900</color>
-    <color name="material_color_secondary_fixed_dim">@android:color/system_accent2_200</color>
-    <color name="material_color_on_error_container">#410000</color>
-    <color name="material_color_on_secondary_fixed">@android:color/system_accent2_900</color>
-    <color name="material_color_on_surface_inverse">@android:color/system_neutral1_100</color>
-    <color name="material_color_tertiary_fixed_dim">@android:color/system_accent3_200</color>
-    <color name="material_color_on_tertiary_fixed">@android:color/system_accent3_900</color>
-    <color name="material_color_primary_fixed_dim">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary_container">@android:color/system_accent2_100</color>
-    <color name="material_color_error_container">#FFDAD5</color>
-    <color name="material_color_on_primary_fixed">@android:color/system_accent1_900</color>
-    <color name="material_color_primary_inverse">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary_fixed">@android:color/system_accent2_100</color>
-    <color name="material_color_tertiary_container">@android:color/system_accent3_100</color>
-    <color name="material_color_tertiary_fixed">@android:color/system_accent3_100</color>
-    <color name="material_color_primary_container">@android:color/system_accent1_100</color>
-    <color name="material_color_on_background">@android:color/system_neutral1_50</color>
-    <color name="material_color_primary_fixed">@android:color/system_accent1_100</color>
-    <color name="material_color_on_secondary">@android:color/system_accent2_0</color>
-    <color name="material_color_on_tertiary">@android:color/system_accent3_0</color>
-    <color name="material_color_on_error">#FFFFFF</color>
-    <color name="material_color_on_surface_variant">@android:color/system_neutral2_700</color>
-    <color name="material_color_outline">@android:color/system_neutral2_500</color>
-    <color name="material_color_outline_variant">@android:color/system_neutral2_200</color>
-    <color name="material_color_on_primary">@android:color/system_accent1_0</color>
     <color name="material_color_on_surface">@android:color/system_neutral1_900</color>
-    <color name="material_color_primary">@android:color/system_accent1_600</color>
-    <color name="material_color_secondary">@android:color/system_accent2_600</color>
-    <color name="material_color_tertiary">@android:color/system_accent3_600</color>
 </resources>
diff --git a/res/values-v34/colors.xml b/res/values-v34/colors.xml
index 26d3712..4f3a769 100644
--- a/res/values-v34/colors.xml
+++ b/res/values-v34/colors.xml
@@ -27,4 +27,108 @@
         @android:color/system_on_surface_light</color>
     <color name="widget_cell_subtitle_color_light">
         @android:color/system_on_surface_variant_light</color>
+    <color name="widget_picker_menu_options_color_light">
+        @android:color/system_on_surface_variant_light
+    </color>
+
+    <color name="system_primary_container_light">@android:color/system_primary_container_light</color>
+    <color name="system_on_primary_container_light">@android:color/system_on_primary_container_light</color>
+    <color name="system_primary_light">@android:color/system_primary_light</color>
+    <color name="system_on_primary_light">@android:color/system_on_primary_light</color>
+    <color name="system_secondary_container_light">@android:color/system_secondary_container_light</color>
+    <color name="system_on_secondary_container_light">@android:color/system_on_secondary_container_light</color>
+    <color name="system_secondary_light">@android:color/system_secondary_light</color>
+    <color name="system_on_secondary_light">@android:color/system_on_secondary_light</color>
+    <color name="system_tertiary_container_light">@android:color/system_tertiary_container_light</color>
+    <color name="system_on_tertiary_container_light">@android:color/system_on_tertiary_container_light</color>
+    <color name="system_tertiary_light">@android:color/system_tertiary_light</color>
+    <color name="system_on_tertiary_light">@android:color/system_on_tertiary_light</color>
+    <color name="system_background_light">@android:color/system_background_light</color>
+    <color name="system_on_background_light">@android:color/system_on_background_light</color>
+    <color name="system_surface_light">@android:color/system_surface_light</color>
+    <color name="system_on_surface_light">@android:color/system_on_surface_light</color>
+    <color name="system_surface_container_low_light">@android:color/system_surface_container_low_light</color>
+    <color name="system_surface_container_lowest_light">@android:color/system_surface_container_lowest_light</color>
+    <color name="system_surface_container_light">@android:color/system_surface_container_light</color>
+    <color name="system_surface_container_high_light">@android:color/system_surface_container_high_light</color>
+    <color name="system_surface_container_highest_light">@android:color/system_surface_container_highest_light</color>
+    <color name="system_surface_bright_light">@android:color/system_surface_bright_light</color>
+    <color name="system_surface_dim_light">@android:color/system_surface_dim_light</color>
+    <color name="system_surface_variant_light">@android:color/system_surface_variant_light</color>
+    <color name="system_on_surface_variant_light">@android:color/system_on_surface_variant_light</color>
+    <color name="system_outline_light">@android:color/system_outline_light</color>
+    <color name="system_outline_variant_light">@android:color/system_outline_variant_light</color>
+    <color name="system_error_light">@android:color/system_error_light</color>
+    <color name="system_on_error_light">@android:color/system_on_error_light</color>
+    <color name="system_error_container_light">@android:color/system_error_container_light</color>
+    <color name="system_on_error_container_light">@android:color/system_on_error_container_light</color>
+    <color name="system_control_activated_light">@android:color/system_control_activated_light</color>
+    <color name="system_control_normal_light">@android:color/system_control_normal_light</color>
+    <color name="system_control_highlight_light">@android:color/system_control_highlight_light</color>
+    <color name="system_text_primary_inverse_light">@android:color/system_text_primary_inverse_light</color>
+    <color name="system_text_secondary_and_tertiary_inverse_light">@android:color/system_text_secondary_and_tertiary_inverse_light</color>
+    <color name="system_text_primary_inverse_disable_only_light">@android:color/system_text_primary_inverse_disable_only_light</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_light">@android:color/system_text_secondary_and_tertiary_inverse_disabled_light</color>
+    <color name="system_text_hint_inverse_light">@android:color/system_text_hint_inverse_light</color>
+    <color name="system_palette_key_color_primary_light">@android:color/system_palette_key_color_primary_light</color>
+    <color name="system_palette_key_color_secondary_light">@android:color/system_palette_key_color_secondary_light</color>
+    <color name="system_palette_key_color_tertiary_light">@android:color/system_palette_key_color_tertiary_light</color>
+    <color name="system_palette_key_color_neutral_light">@android:color/system_palette_key_color_neutral_light</color>
+    <color name="system_palette_key_color_neutral_variant_light">@android:color/system_palette_key_color_neutral_variant_light</color>
+    <color name="system_primary_container_dark">@android:color/system_primary_container_dark</color>
+    <color name="system_on_primary_container_dark">@android:color/system_on_primary_container_dark</color>
+    <color name="system_primary_dark">@android:color/system_primary_dark</color>
+    <color name="system_on_primary_dark">@android:color/system_on_primary_dark</color>
+    <color name="system_secondary_container_dark">@android:color/system_secondary_container_dark</color>
+    <color name="system_on_secondary_container_dark">@android:color/system_on_secondary_container_dark</color>
+    <color name="system_secondary_dark">@android:color/system_secondary_dark</color>
+    <color name="system_on_secondary_dark">@android:color/system_on_secondary_dark</color>
+    <color name="system_tertiary_container_dark">@android:color/system_tertiary_container_dark</color>
+    <color name="system_on_tertiary_container_dark">@android:color/system_on_tertiary_container_dark</color>
+    <color name="system_tertiary_dark">@android:color/system_tertiary_dark</color>
+    <color name="system_on_tertiary_dark">@android:color/system_on_tertiary_dark</color>
+    <color name="system_background_dark">@android:color/system_background_dark</color>
+    <color name="system_on_background_dark">@android:color/system_on_background_dark</color>
+    <color name="system_surface_dark">@android:color/system_surface_dark</color>
+    <color name="system_on_surface_dark">@android:color/system_on_surface_dark</color>
+    <color name="system_surface_container_low_dark">@android:color/system_surface_container_low_dark</color>
+    <color name="system_surface_container_lowest_dark">@android:color/system_surface_container_lowest_dark</color>
+    <color name="system_surface_container_dark">@android:color/system_surface_container_dark</color>
+    <color name="system_surface_container_high_dark">@android:color/system_surface_container_high_dark</color>
+    <color name="system_surface_container_highest_dark">@android:color/system_surface_container_highest_dark</color>
+    <color name="system_surface_bright_dark">@android:color/system_surface_bright_dark</color>
+    <color name="system_surface_dim_dark">@android:color/system_surface_dim_dark</color>
+    <color name="system_surface_variant_dark">@android:color/system_surface_variant_dark</color>
+    <color name="system_on_surface_variant_dark">@android:color/system_on_surface_variant_dark</color>
+    <color name="system_outline_dark">@android:color/system_outline_dark</color>
+    <color name="system_outline_variant_dark">@android:color/system_outline_variant_dark</color>
+    <color name="system_error_dark">@android:color/system_error_dark</color>
+    <color name="system_on_error_dark">@android:color/system_on_error_dark</color>
+    <color name="system_error_container_dark">@android:color/system_error_container_dark</color>
+    <color name="system_on_error_container_dark">@android:color/system_on_error_container_dark</color>
+    <color name="system_control_activated_dark">@android:color/system_control_activated_dark</color>
+    <color name="system_control_normal_dark">@android:color/system_control_normal_dark</color>
+    <color name="system_control_highlight_dark">@android:color/system_control_highlight_dark</color>
+    <color name="system_text_primary_inverse_dark">@android:color/system_text_primary_inverse_dark</color>
+    <color name="system_text_secondary_and_tertiary_inverse_dark">@android:color/system_text_secondary_and_tertiary_inverse_dark</color>
+    <color name="system_text_primary_inverse_disable_only_dark">@android:color/system_text_primary_inverse_disable_only_dark</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_dark">@android:color/system_text_secondary_and_tertiary_inverse_disabled_dark</color>
+    <color name="system_text_hint_inverse_dark">@android:color/system_text_hint_inverse_dark</color>
+    <color name="system_palette_key_color_primary_dark">@android:color/system_palette_key_color_primary_dark</color>
+    <color name="system_palette_key_color_secondary_dark">@android:color/system_palette_key_color_secondary_dark</color>
+    <color name="system_palette_key_color_tertiary_dark">@android:color/system_palette_key_color_tertiary_dark</color>
+    <color name="system_palette_key_color_neutral_dark">@android:color/system_palette_key_color_neutral_dark</color>
+    <color name="system_palette_key_color_neutral_variant_dark">@android:color/system_palette_key_color_neutral_variant_dark</color>
+    <color name="system_primary_fixed">@android:color/system_primary_fixed</color>
+    <color name="system_primary_fixed_dim">@android:color/system_primary_fixed_dim</color>
+    <color name="system_on_primary_fixed">@android:color/system_on_primary_fixed</color>
+    <color name="system_on_primary_fixed_variant">@android:color/system_on_primary_fixed_variant</color>
+    <color name="system_secondary_fixed">@android:color/system_secondary_fixed</color>
+    <color name="system_secondary_fixed_dim">@android:color/system_secondary_fixed_dim</color>
+    <color name="system_on_secondary_fixed">@android:color/system_on_secondary_fixed</color>
+    <color name="system_on_secondary_fixed_variant">@android:color/system_on_secondary_fixed_variant</color>
+    <color name="system_tertiary_fixed">@android:color/system_tertiary_fixed</color>
+    <color name="system_tertiary_fixed_dim">@android:color/system_tertiary_fixed_dim</color>
+    <color name="system_on_tertiary_fixed">@android:color/system_on_tertiary_fixed</color>
+    <color name="system_on_tertiary_fixed_variant">@android:color/system_on_tertiary_fixed_variant</color>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 172c995..cea93da 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Tiện ích bị vô hiệu hóa ở chế độ an toàn"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Lối tắt không khả dụng"</string>
     <string name="home_screen" msgid="5629429142036709174">"Màn hình chính"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Đặt <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> làm trình chạy mặc định trong phần Cài đặt"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Chia đôi màn hình"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Thông tin ứng dụng cho %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Chế độ cài đặt mức sử dụng %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Cửa sổ mới"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Lưu cặp ứng dụng"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cặp ứng dụng này không hoạt động được trên thiết bị này"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Hiện không có cặp ứng dụng này"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Chạm và giữ để di chuyển một tiện ích."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Nhấn đúp và giữ để di chuyển một tiện ích hoặc sử dụng các thao tác tùy chỉnh."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Lựa chọn khác"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Hiện tất cả tiện ích"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Rộng %1$d x cao %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Tiện ích <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ghi chú"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Thêm"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Thêm tiện ích <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Nhấn để thay đổi chế độ cài đặt tiện ích"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Thay đổi chế độ cài đặt tiện ích"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Tìm kiếm ứng dụng"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Cài đặt"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Không gợi ý ứng dụng"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Ghim ứng dụng dự đoán"</string>
+    <string name="bubble" msgid="3072951361014076670">"Bong bóng"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"cài đặt lối tắt"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Cho phép ứng dụng thêm lối tắt mà không cần sự can thiệp của người dùng."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"đọc lối tắt và các chế độ cài đặt trên màn hình chính"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Bị tắt bởi quản trị viên của bạn"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Cho phép xoay màn hình chính"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Khi xoay điện thoại"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Dấu chấm thông báo"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Đang bật"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Tắt"</string>
@@ -128,7 +141,7 @@
     <string name="msg_missing_notification_access" msgid="281113995110910548">"Để hiển thị Dấu chấm thông báo, hãy bật thông báo ứng dụng cho <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="title_change_settings" msgid="1376365968844349552">"Thay đổi cài đặt"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"Hiện dấu chấm thông báo"</string>
-    <string name="developer_options_title" msgid="700788437593726194">"Tùy chọn cho nhà phát triển"</string>
+    <string name="developer_options_title" msgid="700788437593726194">"Tuỳ chọn cho nhà phát triển"</string>
     <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"Thêm biểu tượng ứng dụng vào màn hình chính"</string>
     <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"Cho ứng dụng mới"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"Không xác định"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Đang cài đặt <xliff:g id="NAME">%1$s</xliff:g>, hoàn tất <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Đang tải xuống <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> hoàn tất"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Đang chờ cài đặt <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> đã được lưu trữ. Hãy nhấn để tải xuống và khôi phục."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> đã được lưu trữ."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"tải xuống và khôi phục"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Cần cập nhật ứng dụng"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Ứng dụng cho biểu tượng này chưa được cập nhật. Bạn có thể cập nhật theo cách thủ công để bật lại phím tắt này hoặc xóa biểu tượng."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Cập nhật"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Tôi hiểu"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Tạm dừng các ứng dụng công việc"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Bỏ tạm dừng"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Bộ lọc"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Không thực hiện được thao tác: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Không gian riêng tư"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 9c658dc..9efd6c3 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"安全模式下不允许使用微件"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"无法使用快捷方式"</string>
     <string name="home_screen" msgid="5629429142036709174">"主屏幕"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"在“设置”中将“<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>”设为默认主屏幕应用"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分屏"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s 的应用信息"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s的使用设置"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新窗口"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"保存应用组合"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"在该设备上无法使用此应用对"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"应用对不可用"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"轻触并按住即可移动微件。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"点按两次并按住微件即可移动该微件或使用自定义操作。"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"更多选项"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"显示所有微件"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"宽 %1$d,高 %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"“<xliff:g id="WIDGET_NAME">%1$s</xliff:g>”微件"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"记事"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"添加"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"添加“<xliff:g id="WIDGET_NAME">%1$s</xliff:g>”微件"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"点按即可更改微件设置"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"更改微件设置"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"搜索应用"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"安装"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"不要推荐此应用"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"固定预测的应用"</string>
+    <string name="bubble" msgid="3072951361014076670">"气泡框"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"安装快捷方式"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"允许应用自行添加快捷方式。"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"读取主屏幕设置和快捷方式"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"已被您的管理员停用"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"允许旋转主屏幕"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"手机旋转时"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知圆点"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"已开启"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"已关闭"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"正在安装<xliff:g id="NAME">%1$s</xliff:g>,已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"正在下载<xliff:g id="NAME">%1$s</xliff:g>,已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g>正在等待安装"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"已归档“<xliff:g id="NAME">%1$s</xliff:g>”。点按即可进行下载并恢复。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"已归档“<xliff:g id="NAME">%1$s</xliff:g>”。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"下载并恢复"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"需要更新应用"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"此图标对应的应用未更新。您可以手动更新以重新启用该快捷方式,或者移除此图标。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"知道了"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"暂停工作应用"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"取消暂停"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"过滤器"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"失败:<xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"私密空间"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index af32638..c944240 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"在安全模式中無法使用小工具"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"沒有可用的捷徑"</string>
     <string name="home_screen" msgid="5629429142036709174">"主畫面"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"在「設定」中將「<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>」設定為預設主頁應用程式"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割螢幕"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s 的應用程式資料"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"「%1$s」的用量設定"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新視窗"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"儲存應用程式配對"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"此裝置不支援此應用程式配對"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"應用程式配對無法使用"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"輕觸並按住即可移動小工具。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"㩒兩下之後㩒住,就可以郁小工具或者用自訂操作。"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"更多選項"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"顯示所有小工具"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d 闊,%2$d 高"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"「<xliff:g id="WIDGET_NAME">%1$s</xliff:g>」小工具"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"做筆記"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"新增"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"加<xliff:g id="WIDGET_NAME">%1$s</xliff:g>小工具"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"輕按即可變更小工具設定"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"變更小工具設定"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"搜尋應用程式"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"安裝"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"不要提供應用程式建議"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"固定預測"</string>
+    <string name="bubble" msgid="3072951361014076670">"氣泡"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"安裝捷徑"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"允許應用程式無需使用者許可也可新增捷徑。"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"讀取主畫面設定和捷徑"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"已由你的管理員停用"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"允許旋轉主畫面"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"隨手機旋轉"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知圓點"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"開啟"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"關閉"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"正在安裝「<xliff:g id="NAME">%1$s</xliff:g>」(已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"正在下載 <xliff:g id="NAME">%1$s</xliff:g>,已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"正在等待安裝 <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。輕按即可下載並還原。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"下載及還原"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"必須更新應用程式"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"你尚未更新這個圖示代表的應用程式。你可以手動更新以重新啟用此快速鍵,或者移除圖示。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"知道了"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"暫停工作應用程式"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"取消暫停"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"篩選器"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"操作失敗:<xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"私人空間"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 54a1c5d..476c0e5 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"在安全模式下無法使用小工具"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"目前無法使用捷徑"</string>
     <string name="home_screen" msgid="5629429142036709174">"主畫面"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"前往「設定」將「<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>」設為預設主畫面應用程式"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割畫面"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"「%1$s」的應用程式資訊"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"「%1$s」的用量設定"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新視窗"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"儲存應用程式配對"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"這部裝置不支援這組應用程式配對"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"這個應用程式配對無法使用"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"按住即可移動小工具。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"輕觸兩下並按住即可移動小工具或使用自訂操作。"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"更多選項"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"顯示所有小工具"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"寬度為 %1$d,高度為 %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"「<xliff:g id="WIDGET_NAME">%1$s</xliff:g>」小工具"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"做筆記"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"新增"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"新增「<xliff:g id="WIDGET_NAME">%1$s</xliff:g>」小工具"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"輕觸即可變更小工具設定"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"變更小工具設定"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"搜尋應用程式"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"安裝"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"不要建議此應用程式"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"固定預測的應用程式"</string>
+    <string name="bubble" msgid="3072951361014076670">"泡泡"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"安裝捷徑"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"允許應用程式自動新增捷徑。"</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"讀取主畫面設定和捷徑"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"已由你的管理員停用"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"允許旋轉主畫面"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"當手機旋轉時"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知圓點"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"開啟"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"關閉"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"正在安裝「<xliff:g id="NAME">%1$s</xliff:g>」(已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"正在下載「<xliff:g id="NAME">%1$s</xliff:g>」,已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"正在等待安裝「<xliff:g id="NAME">%1$s</xliff:g>」"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。輕觸即可下載並還原。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"下載及還原"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"必須更新應用程式"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"這個圖示代表的應用程式未更新。手動更新即可重新啟用這個捷徑,你也可以移除圖示。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"我知道了"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"暫停工作應用程式"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"取消暫停"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"篩選器"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"失敗:<xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"私人空間"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 9752e18..623454b 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -27,11 +27,11 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Amawijethi akhutshaziwe kwimodi yokuphepha"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Isinqamuleli asitholakali"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ikhaya"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Setha i-<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> njenge-app yasekhaya ezenzakalelayo Kumasethingi"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Hlukanisa isikrini"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Ulwazi lwe-App ye-%1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Amasethingi okusetshenziswa ka-%1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Iwindi Elisha"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Londoloza i-app ebhangqiwe"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Lokhu kubhanqwa kwe-app akusekelwa kule divayisi"</string>
@@ -39,6 +39,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Ukubhangqwa kwe-app akutholakali"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Thinta uphinde ubambe ukuze uhambise iwijethi."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Thepha kabili uphinde ubambe ukuze uhambise iwijethi noma usebenzise izindlela ezingokwezifiso."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Okungakhethwa kukho okuningi"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Bonisa wonke amawijethi"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ububanzi ngokungu-%2$d ukuya phezulu"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Iwijethi elingu-<xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ukuthatha amanothi"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Engeza"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Engeza iwijethi ye-<xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Thepha ukuze ushintshe amasethingi ewijethi"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Shintsha amasethingi ewijethi"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Sesha izinhlelo zokusebenza"</string>
@@ -91,6 +99,7 @@
     <string name="install_drop_target_label" msgid="2539096853673231757">"Faka"</string>
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ungaphakamisi uhlelo lokusebenza"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Ukubikezela Iphinikhodi"</string>
+    <string name="bubble" msgid="3072951361014076670">"Ibhamuza"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"faka izinqamuleli"</string>
     <string name="permdesc_install_shortcut" msgid="923466509822011139">"Ivumela uhlelo lokusebenza ukufaka izinqamuleli ngaphandle kokungenelela komsebenzisi."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"funda amasethingi wasekhaya nezinqamuleli"</string>
@@ -121,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Kukhutshazwe umlawuli wakho"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Vumela ukuzungezisa kwesikrini sasekhaya"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Uma ifoni iphendukiswa"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Amacashazi esaziso"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Vuliwe"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Valiwe"</string>
@@ -139,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"I-<xliff:g id="NAME">%1$s</xliff:g> iyafakwa, seyiqede <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"I-<xliff:g id="NAME">%1$s</xliff:g> iyalandwa, <xliff:g id="PROGRESS">%2$s</xliff:g> kuqediwe"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ilinde ukufakwa"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Okuthi <xliff:g id="NAME">%1$s</xliff:g> kufakwe kungobo yomlando. Thepha ukuze udawunilode futhi ubuyisele."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Okuthi <xliff:g id="NAME">%1$s</xliff:g> kufakwe kungobo yomlando."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"dawuniloda uphinde ubuyisele"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kudingeka isibuyekezo se-app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"I-app yalesi sithonjana ibuyekeziwe. Ungabuyekeza mathupha ukuze uphinde unike amandla lesi sinqamuleli, noma ususe isithonjana."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Vuselela"</string>
@@ -184,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ngiyezwa"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Misa ama-app omsebenzi"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Susa ukumisa"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Hlunga"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Yehlulekile: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Isikhala esiyimfihlo"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e4e047e..77d789f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -25,7 +25,6 @@
     <attr name="popupColorPrimary" format="color" />
     <attr name="popupColorSecondary" format="color" />
     <attr name="popupColorTertiary" format="color" />
-    <attr name="popupColorBackground" format="color" />
     <attr name="popupTextColor" format="color" />
     <attr name="popupShadeFirst" format="color" />
     <attr name="popupShadeSecond" format="color" />
@@ -45,6 +44,55 @@
     <attr name="focusOutlineColor" format="color" />
     <attr name="focusInnerOutlineColor" format="color" />
 
+    <!--    Recreating Dynamic Color attributes found in the system. This should be the way to go on
+     all launcher projects since they all inherit from launcher3. Avoid creating other color
+     attributes if these can be user directly. -->
+    <attr name="materialColorOnSecondaryFixedVariant" format="color" />
+    <attr name="materialColorOnTertiaryFixedVariant" format="color" />
+    <attr name="materialColorSurfaceContainerLowest" format="color" />
+    <attr name="materialColorOnPrimaryFixedVariant" format="color" />
+    <attr name="materialColorOnSecondaryContainer" format="color" />
+    <attr name="materialColorOnTertiaryContainer" format="color" />
+    <attr name="materialColorSurfaceContainerLow" format="color" />
+    <attr name="materialColorOnPrimaryContainer" format="color" />
+    <attr name="materialColorSecondaryFixedDim" format="color" />
+    <attr name="materialColorOnErrorContainer" format="color" />
+    <attr name="materialColorOnSecondaryFixed" format="color" />
+    <attr name="materialColorOnSurfaceInverse" 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="materialColorSecondaryFixed" format="color" />
+    <attr name="materialColorSurfaceInverse" format="color" />
+    <attr name="materialColorSurfaceVariant" format="color" />
+    <attr name="materialColorTertiaryContainer" format="color" />
+    <attr name="materialColorTertiaryFixed" format="color" />
+    <attr name="materialColorPrimaryContainer" format="color" />
+    <attr name="materialColorOnBackground" format="color" />
+    <attr name="materialColorPrimaryFixed" format="color" />
+    <attr name="materialColorOnSecondary" format="color" />
+    <attr name="materialColorOnTertiary" format="color" />
+    <attr name="materialColorSurfaceDim" format="color" />
+    <attr name="materialColorSurfaceBright" format="color" />
+    <attr name="materialColorOnError" format="color" />
+    <attr name="materialColorSurface" format="color" />
+    <attr name="materialColorSurfaceContainerHigh" format="color" />
+    <attr name="materialColorSurfaceContainerHighest" format="color" />
+    <attr name="materialColorOnSurfaceVariant" format="color" />
+    <attr name="materialColorOutline" format="color" />
+    <attr name="materialColorOutlineVariant" format="color" />
+    <attr name="materialColorOnPrimary" format="color" />
+    <attr name="materialColorOnSurface" format="color" />
+    <attr name="materialColorSurfaceContainer" format="color" />
+    <attr name="materialColorPrimary" format="color" />
+    <attr name="materialColorSecondary" format="color" />
+    <attr name="materialColorTertiary" format="color" />
+    <attr name="materialColorError" format="color" />
+
     <attr name="pageIndicatorDotColor" format="color" />
     <attr name="folderPreviewColor" format="color" />
     <attr name="folderBackgroundColor" format="color" />
@@ -62,6 +110,7 @@
     <attr name="preloadIconBackgroundColor" format="color" />
     <attr name="widgetPickerTitleColor" format="color"/>
     <attr name="widgetPickerDescriptionColor" format="color"/>
+    <attr name="widgetPickerWidgetOptionsMenuColor" format="color"/>
     <attr name="widgetPickerPrimarySurfaceColor" format="color"/>
     <attr name="widgetPickerSecondarySurfaceColor" format="color"/>
     <attr name="widgetPickerHeaderAppTitleColor" format="color"/>
@@ -77,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" />
 
@@ -159,8 +210,18 @@
         <attr name="layout_sticky" format="boolean" />
     </declare-styleable>
 
+    <declare-styleable name="NumRows">
+        <attr name="minDeviceWidthPx" format="float"/>
+        <attr name="minDeviceHeightPx" format="float"/>
+        <attr name="numRowsNew" format="integer"/>
+        <attr name="dbFile" />
+        <attr name="defaultLayoutId"/>
+        <attr name="demoModeLayoutId"/>
+    </declare-styleable>
+
     <declare-styleable name="GridDisplayOption">
         <attr name="name" format="string" />
+        <attr name="title" />
 
         <attr name="numRows" format="integer" />
         <attr name="numColumns" format="integer" />
@@ -211,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" />
@@ -245,9 +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" />
+
         <!-- By default all categories are enabled -->
         <attr name="deviceCategory" format="integer">
             <!-- Enable on phone only -->
@@ -581,51 +649,6 @@
         <attr name="collapsable" format="boolean" />
     </declare-styleable>
 
-    <attr name="materialColorOnSecondaryFixedVariant" format="color" />
-    <attr name="materialColorOnTertiaryFixedVariant" format="color" />
-    <attr name="materialColorSurfaceContainerLowest" format="color" />
-    <attr name="materialColorOnPrimaryFixedVariant" format="color" />
-    <attr name="materialColorOnSecondaryContainer" format="color" />
-    <attr name="materialColorOnTertiaryContainer" format="color" />
-    <attr name="materialColorSurfaceContainerLow" format="color" />
-    <attr name="materialColorOnPrimaryContainer" format="color" />
-    <attr name="materialColorSecondaryFixedDim" format="color" />
-    <attr name="materialColorOnErrorContainer" format="color" />
-    <attr name="materialColorOnSecondaryFixed" format="color" />
-    <attr name="materialColorOnSurfaceInverse" 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="materialColorSecondaryFixed" format="color" />
-    <attr name="materialColorTertiaryContainer" format="color" />
-    <attr name="materialColorTertiaryFixed" format="color" />
-    <attr name="materialColorPrimaryContainer" format="color" />
-    <attr name="materialColorOnBackground" format="color" />
-    <attr name="materialColorPrimaryFixed" format="color" />
-    <attr name="materialColorOnSecondary" format="color" />
-    <attr name="materialColorOnTertiary" format="color" />
-    <attr name="materialColorOnError" format="color" />
-    <attr name="materialColorOnSurfaceVariant" format="color" />
-    <attr name="materialColorOutline" format="color" />
-    <attr name="materialColorOutlineVariant" format="color" />
-    <attr name="materialColorOnPrimary" format="color" />
-    <attr name="materialColorOnSurface" format="color" />
-    <attr name="materialColorPrimary" format="color" />
-    <attr name="materialColorSecondary" format="color" />
-    <attr name="materialColorTertiary" format="color" />
-    <attr name="materialColorSurfaceInverse" format="color" />
-    <attr name="materialColorSurfaceVariant" format="color" />
-    <attr name="materialColorSurfaceDim" format="color" />
-    <attr name="materialColorSurfaceBright" format="color" />
-    <attr name="materialColorSurface" format="color" />
-    <attr name="materialColorSurfaceContainerHigh" format="color" />
-    <attr name="materialColorSurfaceContainerHighest" format="color" />
-    <attr name="materialColorSurfaceContainer" format="color" />
-
     <declare-styleable name="WidgetSections">
         <!-- Component name of an app widget provider. -->
         <attr name="provider" format="string" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 8fa1992..4549b86 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,8 +17,7 @@
 ** limitations under the License.
 */
 -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- The color tints to apply to the text and drag view when hovering
          over the delete target or the info target -->
     <color name="delete_target_hover_tint">#FFC1C1C1</color>
@@ -91,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>
@@ -98,13 +98,12 @@
     <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>
     <color name="widget_picker_title_color_light">#1F1F1F</color>
     <color name="widget_picker_description_color_light">#4C4D50</color>
+    <color name="widget_picker_menu_options_color_light">@color/system_on_surface_variant_light</color>
     <color name="widget_picker_header_app_title_color_light">#1F1F1F</color>
     <color name="widget_picker_header_app_subtitle_color_light">#444746</color>
     <color name="widget_picker_header_background_color_light">#C2E7FF</color>
@@ -118,13 +117,20 @@
     <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_cell_title_color_light">@color/material_color_on_surface</color>
-    <color name="widget_cell_subtitle_color_light">@color/material_color_on_surface_variant</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>
 
     <color name="widget_picker_primary_surface_color_dark">#1F2020</color>
     <color name="widget_picker_secondary_surface_color_dark">#393939</color>
     <color name="widget_picker_title_color_dark">#E3E3E3</color>
     <color name="widget_picker_description_color_dark">#CCCDCF</color>
+    <color name="widget_picker_menu_options_color_dark">@color/system_on_surface_variant_dark</color>
     <color name="widget_picker_header_app_title_color_dark">#E3E3E3</color>
     <color name="widget_picker_header_app_subtitle_color_dark">#C4C7C5</color>
     <color name="widget_picker_header_background_color_dark">#004A77</color>
@@ -138,51 +144,116 @@
     <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_cell_title_color_dark">@color/material_color_on_surface</color>
-    <color name="widget_cell_subtitle_color_dark">@color/material_color_on_surface_variant</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>
 
-    <color name="material_color_on_secondary_fixed_variant">#3F4759</color>
-    <color name="material_color_on_tertiary_fixed_variant">#583E5B</color>
     <color name="material_color_surface_container_lowest">#FFFFFF</color>
-    <color name="material_color_on_primary_fixed_variant">#2B4678</color>
-    <color name="material_color_on_secondary_container">#141B2C</color>
-    <color name="material_color_on_tertiary_container">#29132D</color>
-    <color name="material_color_surface_container_low">#F5F3F7</color>
-    <color name="material_color_on_primary_container">#001A41</color>
-    <color name="material_color_secondary_fixed_dim">#BFC6DC</color>
-    <color name="material_color_on_error_container">#410000</color>
-    <color name="material_color_on_secondary_fixed">#141B2C</color>
-    <color name="material_color_on_surface_inverse">#E3E2E6</color>
-    <color name="material_color_tertiary_fixed_dim">#DEBCDF</color>
-    <color name="material_color_on_tertiary_fixed">#29132D</color>
-    <color name="material_color_primary_fixed_dim">#ADC6FF</color>
-    <color name="material_color_secondary_container">#DBE2F9</color>
-    <color name="material_color_error_container">#FFDAD5</color>
-    <color name="material_color_on_primary_fixed">#001A41</color>
-    <color name="material_color_primary_inverse">#ADC6FF</color>
-    <color name="material_color_secondary_fixed">#DBE2F9</color>
-    <color name="material_color_surface_inverse">#121316</color>
-    <color name="material_color_surface_variant">#E1E2EC</color>
-    <color name="material_color_tertiary_container">#FBD7FC</color>
-    <color name="material_color_tertiary_fixed">#FBD7FC</color>
-    <color name="material_color_primary_container">#D8E2FF</color>
-    <color name="material_color_on_background">#1B1B1F</color>
-    <color name="material_color_primary_fixed">#D8E2FF</color>
-    <color name="material_color_on_secondary">#FFFFFF</color>
-    <color name="material_color_on_tertiary">#FFFFFF</color>
-    <color name="material_color_surface_dim">#DBD9DD</color>
-    <color name="material_color_surface_bright">#FAF9FD</color>
-    <color name="material_color_on_error">#FFFFFF</color>
-    <color name="material_color_surface">#FAF9FD</color>
-    <color name="material_color_surface_container_high">#E9E7EC</color>
-    <color name="material_color_surface_container_highest">#E3E2E6</color>
-    <color name="material_color_on_surface_variant">#44474F</color>
-    <color name="material_color_outline">#72747D</color>
-    <color name="material_color_outline_variant">#C4C7C5</color>
-    <color name="material_color_on_primary">#FFFFFF</color>
     <color name="material_color_on_surface">#1B1B1F</color>
-    <color name="material_color_surface_container">#EFEDF1</color>
-    <color name="material_color_primary">#445E91</color>
-    <color name="material_color_secondary">#575E71</color>
-    <color name="material_color_tertiary">#715573</color>
+
+    <color name="system_primary_container_light">#D9E2FF</color>
+    <color name="system_on_primary_container_light">#001945</color>
+    <color name="system_primary_light">#475D92</color>
+    <color name="system_on_primary_light">#FFFFFF</color>
+    <color name="system_secondary_container_light">#DCE2F9</color>
+    <color name="system_on_secondary_container_light">#151B2C</color>
+    <color name="system_secondary_light">#575E71</color>
+    <color name="system_on_secondary_light">#FFFFFF</color>
+    <color name="system_tertiary_container_light">#FDD7FA</color>
+    <color name="system_on_tertiary_container_light">#2A122C</color>
+    <color name="system_tertiary_light">#725572</color>
+    <color name="system_on_tertiary_light">#FFFFFF</color>
+    <color name="system_background_light">#FAF8FF</color>
+    <color name="system_on_background_light">#1A1B20</color>
+    <color name="system_surface_light">#FAF8FF</color>
+    <color name="system_on_surface_light">#1A1B20</color>
+    <color name="system_surface_container_low_light">#F4F3FA</color>
+    <color name="system_surface_container_lowest_light">#FFFFFF</color>
+    <color name="system_surface_container_light">#EEEDF4</color>
+    <color name="system_surface_container_high_light">#E8E7EF</color>
+    <color name="system_surface_container_highest_light">#E2E2E9</color>
+    <color name="system_surface_bright_light">#FAF8FF</color>
+    <color name="system_surface_dim_light">#DAD9E0</color>
+    <color name="system_surface_variant_light">#E1E2EC</color>
+    <color name="system_on_surface_variant_light">#44464F</color>
+    <color name="system_outline_light">#757780</color>
+    <color name="system_outline_variant_light">#C5C6D0</color>
+    <color name="system_error_light">#BA1A1A</color>
+    <color name="system_on_error_light">#FFFFFF</color>
+    <color name="system_error_container_light">#FFDAD6</color>
+    <color name="system_on_error_container_light">#410002</color>
+    <color name="system_control_activated_light">#D9E2FF</color>
+    <color name="system_control_normal_light">#44464F</color>
+    <color name="system_control_highlight_light">#000000</color>
+    <color name="system_text_primary_inverse_light">#E2E2E9</color>
+    <color name="system_text_secondary_and_tertiary_inverse_light">#C5C6D0</color>
+    <color name="system_text_primary_inverse_disable_only_light">#E2E2E9</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_light">#E2E2E9</color>
+    <color name="system_text_hint_inverse_light">#E2E2E9</color>
+    <color name="system_palette_key_color_primary_light">#6076AC</color>
+    <color name="system_palette_key_color_secondary_light">#70778B</color>
+    <color name="system_palette_key_color_tertiary_light">#8C6D8C</color>
+    <color name="system_palette_key_color_neutral_light">#76777D</color>
+    <color name="system_palette_key_color_neutral_variant_light">#757780</color>
+    <color name="system_primary_container_dark">#2F4578</color>
+    <color name="system_on_primary_container_dark">#D9E2FF</color>
+    <color name="system_primary_dark">#B0C6FF</color>
+    <color name="system_on_primary_dark">#152E60</color>
+    <color name="system_secondary_container_dark">#404659</color>
+    <color name="system_on_secondary_container_dark">#DCE2F9</color>
+    <color name="system_secondary_dark">#C0C6DC</color>
+    <color name="system_on_secondary_dark">#2A3042</color>
+    <color name="system_tertiary_container_dark">#593D59</color>
+    <color name="system_on_tertiary_container_dark">#FDD7FA</color>
+    <color name="system_tertiary_dark">#E0BBDD</color>
+    <color name="system_on_tertiary_dark">#412742</color>
+    <color name="system_background_dark">#121318</color>
+    <color name="system_on_background_dark">#E2E2E9</color>
+    <color name="system_surface_dark">#121318</color>
+    <color name="system_on_surface_dark">#E2E2E9</color>
+    <color name="system_surface_container_low_dark">#1A1B20</color>
+    <color name="system_surface_container_lowest_dark">#0C0E13</color>
+    <color name="system_surface_container_dark">#1E1F25</color>
+    <color name="system_surface_container_high_dark">#282A2F</color>
+    <color name="system_surface_container_highest_dark">#33343A</color>
+    <color name="system_surface_bright_dark">#38393F</color>
+    <color name="system_surface_dim_dark">#121318</color>
+    <color name="system_surface_variant_dark">#44464F</color>
+    <color name="system_on_surface_variant_dark">#C5C6D0</color>
+    <color name="system_outline_dark">#8F9099</color>
+    <color name="system_outline_variant_dark">#44464F</color>
+    <color name="system_error_dark">#FFB4AB</color>
+    <color name="system_on_error_dark">#690005</color>
+    <color name="system_error_container_dark">#93000A</color>
+    <color name="system_on_error_container_dark">#FFDAD6</color>
+    <color name="system_control_activated_dark">#2F4578</color>
+    <color name="system_control_normal_dark">#C5C6D0</color>
+    <color name="system_control_highlight_dark">#FFFFFF</color>
+    <color name="system_text_primary_inverse_dark">#1A1B20</color>
+    <color name="system_text_secondary_and_tertiary_inverse_dark">#44464F</color>
+    <color name="system_text_primary_inverse_disable_only_dark">#1A1B20</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_dark">#1A1B20</color>
+    <color name="system_text_hint_inverse_dark">#1A1B20</color>
+    <color name="system_palette_key_color_primary_dark">#6076AC</color>
+    <color name="system_palette_key_color_secondary_dark">#70778B</color>
+    <color name="system_palette_key_color_tertiary_dark">#8C6D8C</color>
+    <color name="system_palette_key_color_neutral_dark">#76777D</color>
+    <color name="system_palette_key_color_neutral_variant_dark">#757780</color>
+    <color name="system_primary_fixed">#D9E2FF</color>
+    <color name="system_primary_fixed_dim">#B0C6FF</color>
+    <color name="system_on_primary_fixed">#001945</color>
+    <color name="system_on_primary_fixed_variant">#2F4578</color>
+    <color name="system_secondary_fixed">#DCE2F9</color>
+    <color name="system_secondary_fixed_dim">#C0C6DC</color>
+    <color name="system_on_secondary_fixed">#151B2C</color>
+    <color name="system_on_secondary_fixed_variant">#404659</color>
+    <color name="system_tertiary_fixed">#FDD7FA</color>
+    <color name="system_tertiary_fixed_dim">#E0BBDD</color>
+    <color name="system_on_tertiary_fixed">#2A122C</color>
+    <color name="system_on_tertiary_fixed_variant">#593D59</color>
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 2a3b588..f6f3c95 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -67,7 +67,6 @@
     <string name="main_process_initializer_class" translatable="false"></string>
     <string name="app_launch_tracker_class" translatable="false"></string>
     <string name="test_information_handler_class" translatable="false"></string>
-    <string name="launcher_activity_logic_class" translatable="false"></string>
     <string name="model_delegate_class" translatable="false"></string>
     <string name="window_manager_proxy_class" translatable="false"></string>
     <string name="secondary_display_predictions_class" translatable="false"></string>
@@ -79,13 +78,14 @@
     <string name="taskbar_edu_tooltip_controller_class" translatable="false"></string>
     <!--  Used for determining category of a widget presented in widget recommendations. -->
     <string name="widget_recommendation_category_provider_class" translatable="false"></string>
-    <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 05724e2..c69778a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -81,6 +81,11 @@
     <dimen name="fastscroll_popup_text_size">32dp</dimen>
     <dimen name="fastscroll_popup_margin">19dp</dimen>
 
+    <dimen name="fastscroll_list_letter_size">5dp</dimen>
+    <dimen name="fastscroll_list_letter_text_size">14sp</dimen>
+    <dimen name="fastscroll_list_letter_end_margin">-10dp</dimen>
+    <dimen name="bg_letter_list_text_size">20sp</dimen>
+
     <!--
       Fast scroller draws the content horizontally centered. The end of the track should be
       aligned at the end of the container.
@@ -121,6 +126,10 @@
     <dimen name="all_apps_work_profile_tab_footer_bottom_padding">20dp</dimen>
     <dimen name="all_apps_tabs_button_horizontal_padding">4dp</dimen>
     <dimen name="all_apps_tabs_vertical_padding">6dp</dimen>
+    <dimen name="all_apps_tabs_vertical_padding_focus">1dp</dimen>
+    <dimen name="all_apps_tabs_focus_width">5dp</dimen>
+    <dimen name="all_apps_tabs_focus_border">3dp</dimen>
+    <dimen name="all_apps_tabs_focus_padding">2dp</dimen>
     <dimen name="all_apps_tabs_margin_top">8dp</dimen>
     <dimen name="all_apps_divider_height">2dp</dimen>
     <dimen name="all_apps_divider_width">128dp</dimen>
@@ -147,15 +156,19 @@
     <dimen name="work_fab_height">56dp</dimen>
     <dimen name="work_fab_radius">16dp</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>
@@ -172,8 +185,6 @@
     <dimen name="padded_rounded_button_height">48dp</dimen>
     <dimen name="rounded_button_height">48dp</dimen>
     <dimen name="rounded_button_radius">200dp</dimen>
-    <dimen name="rounded_button_padding">8dp</dimen>
-
 
     <!-- Widget tray -->
     <dimen name="widget_cell_vertical_padding">8dp</dimen>
@@ -217,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>
@@ -308,6 +329,7 @@
     <dimen name="blur_size_medium_outline">2dp</dimen>
     <dimen name="blur_size_click_shadow">4dp</dimen>
     <dimen name="click_shadow_high_shift">2dp</dimen>
+    <dimen name="app_title_icon_shadow_inset">0.5dp</dimen>
 
     <!-- Pending widget -->
     <dimen name="pending_widget_min_padding">8dp</dimen>
@@ -418,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>
@@ -457,6 +477,7 @@
     <!-- Overview placeholder to compile in Launcher3 without Quickstep -->
     <dimen name="task_thumbnail_icon_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size">0dp</dimen>
+    <dimen name="task_thumbnail_splash_icon_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size_grid">0dp</dimen>
     <dimen name="task_thumbnail_icon_menu_drawable_touch_size">0dp</dimen>
     <dimen name="task_menu_edge_padding">0dp</dimen>
@@ -483,8 +504,6 @@
     <dimen name="split_instructions_bottom_margin_phone_landscape">24dp</dimen>
     <dimen name="split_instructions_bottom_margin_phone_portrait">60dp</dimen>
     <dimen name="split_instructions_start_margin_cancel">8dp</dimen>
-    <dimen name="split_divider_handle_region_width">96dp</dimen>
-    <dimen name="split_divider_handle_region_height">48dp</dimen>
 
     <dimen name="focus_outline_radius">16dp</dimen>
     <dimen name="focus_inner_outline_radius">14dp</dimen>
diff --git a/res/values/id.xml b/res/values/id.xml
index 28496b5..67692d8 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -19,6 +19,7 @@
     <item type="id" name="view_type_widgets_space" />
     <item type="id" name="view_type_widgets_list" />
     <item type="id" name="view_type_widgets_header" />
+    <item type="id" name="view_type_widgets_list_expand" />
 
     <!-- Accessibility actions -->
     <item type="id" name="action_remove" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d33adc4..123e2b8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -45,6 +45,9 @@
     <string name="split_app_info_accessibility">App info for %1$s</string>
     <string name="split_app_usage_settings">Usage settings for %1$s</string>
 
+    <!-- Title for an option to open a new window for a given app   -->
+    <string name="new_window_option_taskbar">New Window</string>
+
     <!-- App pairs -->
     <string name="save_app_pair">Save app pair</string>
     <!-- App pair default title -->
@@ -61,6 +64,12 @@
     <string name="long_press_widget_to_add">Touch &amp; hold to move a widget.</string>
     <!-- Accessibility spoken hint message in widget picker, which allows user to add a widget. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=100] -->
     <string name="long_accessible_way_to_add">Double-tap &amp; hold to move a widget or use custom actions.</string>
+    <!-- Accessibility label for the icon button shown in the widget picker that opens a overflow
+         menu with widgets viewing options. [CHAR_LIMIT=25] -->
+    <string name="widget_picker_widget_options_button_description">More options</string>
+    <!-- Label for the checkbox shown in the widget picker toggles whether to show all widgets or
+     the default set. [CHAR_LIMIT=25] -->
+    <string name="widget_picker_show_all_widgets_menu_item_title">Show all widgets</string>
     <!-- The format string for the dimensions of a widget in the drawer -->
     <!-- There is a special version of this format string for Farsi -->
     <string name="widget_dims_format">%1$d \u00d7 %2$d</string>
@@ -148,6 +157,12 @@
     <!-- Accessibility content description for the button that adds a widget to the home screen. The
          placeholder text is the widget name. [CHAR_LIMIT=none] -->
     <string name="widget_add_button_content_description">Add <xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget</string>
+    <!-- Text on the button that enables users to expand the widgets list to see all widget apps besides the default ones displayed. [CHAR_LIMIT=15] -->
+    <string name="widgets_list_expand_button_label">Show all</string>
+    <!-- Accessibility content description for the button that enables users to expand the widgets list to see all widget apps besides the default ones displayed. [CHAR_LIMIT=none]  -->
+    <string name="widgets_list_expand_button_content_description">Show all widgets</string>
+    <!-- Accessibility announcement to indicate to the users that widgets list is now expanded -->
+    <string name="widgets_list_expanded">Showing all widgets</string>
 
     <!-- Text on an educational tip on widget informing users that they can change widget settings.
          [CHAR_LIMIT=NONE] -->
@@ -209,7 +224,8 @@
     <string name="dismiss_prediction_label">Don\'t suggest app</string>
     <!-- Label for pinning predicted app. -->
     <string name="pin_prediction">Pin Prediction</string>
-
+    <!-- Label for bubbling a launcher item. [CHAR_LIMIT=20] -->
+    <string name="bubble">Bubble</string>
 
     <!-- Permissions: -->
     <skip />
@@ -301,6 +317,12 @@
     <string name="allow_rotation_title">Allow home screen rotation</string>
     <!-- Text explaining when the home screen will get rotated. [CHAR LIMIT=100] -->
     <string name="allow_rotation_desc">When phone is rotated</string>
+
+    <!-- Title for Landscape Mode setting. [CHAR LIMIT=50] -->
+    <string name="landscape_mode_title">Landscape mode</string>
+    <!--  [CHAR LIMIT=100] -->
+    <string name="landscape_mode_desc">Set phone into landscape mode</string>
+
     <!-- Title for Notification dots setting. Tapping this will link to the system Notifications settings screen where the user can turn off notification dots globally. [CHAR LIMIT=50] -->
     <string name="notification_dots_title">Notification dots</string>
     <!-- Text to indicate that the system notification dots setting is on [CHAR LIMIT=100] -->
@@ -345,8 +367,9 @@
     <!-- Title for an app whose download has been started. -->
     <string name="app_waiting_download_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> waiting to install</string>
     <!-- Title for an app which is archived. -->
-    <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived. Tap to download and restore.</string>
-
+    <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived.</string>
+    <!-- Accessibility Action for an app which is archived. -->
+    <string name="app_unarchiving_action">download and restore</string>
 
     <!-- Title shown on the alert dialog prompting the user to update the application in market
      in order to re-enable the disabled shortcuts -->
@@ -455,6 +478,8 @@
     <string name="work_profile_edu_accept">Got it</string>
     <!-- Info icon unicode for alpha scroller when work edu card is present -->
     <string name="work_profile_edu_section" translatable="false">\u24D8</string>
+    <!-- Intent for work profiler scheduler -->
+    <string name="work_profile_scheduler_intent" translatable="false"/>
 
     <!--- heading shown when user opens work apps tab while work apps are paused -->
     <string name="work_apps_paused_title">Work apps are paused</string>
@@ -473,6 +498,8 @@
     <string name="work_apps_pause_btn_text">Pause work apps</string>
     <!-- button string shown enable work profile -->
     <string name="work_apps_enable_btn_text">Unpause</string>
+    <!-- Label for the work profile scheduler button in the work profile screen. [CHAR_LIMIT=40] -->
+    <string name="work_scheduler_button_content_description">Work apps schedule</string>
 
     <!-- A hint shown in launcher settings develop options filter box -->
     <string name="developer_options_filter_hint">Filter</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index f7273a0..6d3579b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,7 +17,7 @@
 */
 -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- Launcher theme -->
     <style name="BaseLauncherTheme" parent="@android:style/Theme.DeviceDefault.Light">
         <item name="disabledIconAlpha">.54</item>
@@ -29,17 +29,68 @@
         <item name="android:windowShowWallpaper">true</item>
     </style>
 
-    <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
+    <style name="DynamicColorsBaseLauncherTheme" parent="@style/BaseLauncherTheme">
+        <item name="materialColorOnSecondaryFixedVariant">@color/system_on_secondary_fixed_variant</item>
+        <item name="materialColorOnTertiaryFixedVariant">@color/system_on_tertiary_fixed_variant</item>
+        <item name="materialColorSurfaceContainerLowest">@color/system_surface_container_lowest_light</item>
+        <item name="materialColorOnPrimaryFixedVariant">@color/system_on_primary_fixed_variant</item>
+        <item name="materialColorOnSecondaryContainer">@color/system_on_secondary_container_light</item>
+        <item name="materialColorOnTertiaryContainer">@color/system_on_tertiary_container_light</item>
+        <item name="materialColorSurfaceContainerLow">@color/system_surface_container_low_light</item>
+        <item name="materialColorOnPrimaryContainer">@color/system_on_primary_container_light</item>
+        <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="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="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
+        <item name="materialColorSurfaceInverse">@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>
+        <item name="materialColorPrimaryContainer">@color/system_primary_container_light</item>
+        <item name="materialColorOnBackground">@color/system_on_background_light</item>
+        <item name="materialColorPrimaryFixed">@color/system_primary_fixed</item>
+        <item name="materialColorOnSecondary">@color/system_on_secondary_light</item>
+        <item name="materialColorOnTertiary">@color/system_on_tertiary_light</item>
+        <item name="materialColorSurfaceDim">@color/system_surface_dim_light</item>
+        <item name="materialColorSurfaceBright">@color/system_surface_bright_light</item>
+        <item name="materialColorOnError">@color/system_on_error_light</item>
+        <item name="materialColorSurface">@color/system_surface_light</item>
+        <item name="materialColorSurfaceContainerHigh">@color/system_surface_container_high_light</item>
+        <item name="materialColorSurfaceContainerHighest">@color/system_surface_container_highest_light</item>
+        <item name="materialColorOnSurfaceVariant">@color/system_on_surface_variant_light</item>
+        <item name="materialColorOutline">@color/system_outline_light</item>
+        <item name="materialColorOutlineVariant">@color/system_outline_variant_light</item>
+        <item name="materialColorOnPrimary">@color/system_on_primary_light</item>
+        <item name="materialColorOnSurface">@color/system_on_surface_light</item>
+        <item name="materialColorSurfaceContainer">@color/system_surface_container_light</item>
+        <item name="materialColorPrimary">@color/system_primary_light</item>
+        <item name="materialColorSecondary">@color/system_secondary_light</item>
+        <item name="materialColorTertiary">@color/system_tertiary_light</item>
+        <item name="materialColorError">@color/system_error_light</item>
+    </style>
+
+    <style name="DynamicColorsBaseLauncherTheme.NoActionBar">
+        <item name="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+    </style>
+
+    <style name="LauncherTheme" parent="@style/DynamicColorsBaseLauncherTheme">
         <item name="android:textColorSecondary">#DE000000</item>
         <item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
-        <item name="allappsHeaderProtectionColor">
-            @color/material_color_surface_container_highest</item>
+        <item name="allappsHeaderProtectionColor">?attr/materialColorSurfaceContainerHighest</item>
         <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
         <item name="popupColorPrimary">@color/popup_color_primary_light</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_light</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
-        <item name="popupColorBackground">#EFEDED</item>
-        <item name="popupTextColor">@color/popup_text_color_light</item>
+        <item name="popupTextColor">@color/system_on_surface_light</item>
         <item name="popupShadeFirst">@color/popup_shade_first_light</item>
         <item name="popupShadeSecond">@color/popup_shade_second_light</item>
         <item name="popupShadeThird">@color/popup_shade_third_light</item>
@@ -53,15 +104,15 @@
         <item name="workspaceKeyShadowColor">#89000000</item>
         <item name="widgetsTheme">@style/WidgetContainerTheme</item>
         <item name="pageIndicatorDotColor">@color/page_indicator_dot_color_light</item>
-        <item name="focusOutlineColor">@color/material_color_secondary_fixed</item>
-        <item name="focusInnerOutlineColor">@color/material_color_on_secondary_fixed_variant</item>
+        <item name="focusOutlineColor">?attr/materialColorSecondaryFixed</item>
+        <item name="focusInnerOutlineColor">?attr/materialColorOnSecondaryFixedVariant</item>
         <item name="folderPreviewColor">@color/folder_preview_light</item>
         <item name="folderBackgroundColor">@color/folder_background_light</item>
         <item name="folderIconBorderColor">?android:attr/colorPrimary</item>
         <item name="isFolderDarkText">true</item>
         <item name="folderTextColor">@color/folder_text_color_light</item>
         <item name="folderHintTextColor">@color/folder_hint_text_color_light</item>
-        <item name="appPairSurfaceInFolder">@color/material_color_surface_container_lowest</item>
+        <item name="appPairSurfaceInFolder">?attr/materialColorSurfaceContainerLowest</item>
         <item name="loadingIconColor">#CCFFFFFF</item>
         <item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
         <item name="eduHalfSheetBGColor">?android:attr/colorAccent</item>
@@ -78,51 +129,6 @@
         <item name="android:statusBarColor">#00000000</item>
         <item name="android:navigationBarColor">#00000000</item>
         <item name="android:switchStyle">@style/SwitchStyle</item>
-
-        <item name="materialColorOnSecondaryFixedVariant">@color/material_color_on_secondary_fixed_variant</item>
-        <item name="materialColorOnTertiaryFixedVariant">@color/material_color_on_tertiary_fixed_variant</item>
-        <item name="materialColorSurfaceContainerLowest">@color/material_color_surface_container_lowest</item>
-        <item name="materialColorOnPrimaryFixedVariant">@color/material_color_on_primary_fixed_variant</item>
-        <item name="materialColorOnSecondaryContainer">@color/material_color_on_secondary_container</item>
-        <item name="materialColorOnTertiaryContainer">@color/material_color_on_tertiary_container</item>
-        <item name="materialColorSurfaceContainerLow">@color/material_color_surface_container_low</item>
-        <item name="materialColorOnPrimaryContainer">@color/material_color_on_primary_container</item>
-        <item name="materialColorSecondaryFixedDim">@color/material_color_secondary_fixed_dim</item>
-        <item name="materialColorOnErrorContainer">@color/material_color_on_error_container</item>
-        <item name="materialColorOnSecondaryFixed">@color/material_color_on_secondary_fixed</item>
-        <item name="materialColorOnSurfaceInverse">@color/material_color_on_surface_inverse</item>
-        <item name="materialColorTertiaryFixedDim">@color/material_color_tertiary_fixed_dim</item>
-        <item name="materialColorOnTertiaryFixed">@color/material_color_on_tertiary_fixed</item>
-        <item name="materialColorPrimaryFixedDim">@color/material_color_primary_fixed_dim</item>
-        <item name="materialColorSecondaryContainer">@color/material_color_secondary_container</item>
-        <item name="materialColorErrorContainer">@color/material_color_error_container</item>
-        <item name="materialColorOnPrimaryFixed">@color/material_color_on_primary_fixed</item>
-        <item name="materialColorPrimaryInverse">@color/material_color_primary_inverse</item>
-        <item name="materialColorSecondaryFixed">@color/material_color_secondary_fixed</item>
-        <item name="materialColorSurfaceInverse">@color/material_color_surface_inverse</item>
-        <item name="materialColorSurfaceVariant">@color/material_color_surface_variant</item>
-        <item name="materialColorTertiaryContainer">@color/material_color_tertiary_container</item>
-        <item name="materialColorTertiaryFixed">@color/material_color_tertiary_fixed</item>
-        <item name="materialColorPrimaryContainer">@color/material_color_primary_container</item>
-        <item name="materialColorOnBackground">@color/material_color_on_background</item>
-        <item name="materialColorPrimaryFixed">@color/material_color_primary_fixed</item>
-        <item name="materialColorOnSecondary">@color/material_color_on_secondary</item>
-        <item name="materialColorOnTertiary">@color/material_color_on_tertiary</item>
-        <item name="materialColorSurfaceDim">@color/material_color_surface_dim</item>
-        <item name="materialColorSurfaceBright">@color/material_color_surface_bright</item>
-        <item name="materialColorOnError">@color/material_color_on_error</item>
-        <item name="materialColorSurface">@color/material_color_surface</item>
-        <item name="materialColorSurfaceContainerHigh">@color/material_color_surface_container_high</item>
-        <item name="materialColorSurfaceContainerHighest">@color/material_color_surface_container_highest</item>
-        <item name="materialColorOnSurfaceVariant">@color/material_color_on_surface_variant</item>
-        <item name="materialColorOutline">@color/material_color_outline</item>
-        <item name="materialColorOutlineVariant">@color/material_color_outline_variant</item>
-        <item name="materialColorOnPrimary">@color/material_color_on_primary</item>
-        <item name="materialColorOnSurface">@color/material_color_on_surface</item>
-        <item name="materialColorSurfaceContainer">@color/material_color_surface_container</item>
-        <item name="materialColorPrimary">@color/material_color_primary</item>
-        <item name="materialColorSecondary">@color/material_color_secondary</item>
-        <item name="materialColorTertiary">@color/material_color_tertiary</item>
     </style>
 
     <style name="SwitchStyle"
@@ -153,13 +159,12 @@
         <item name="android:colorControlHighlight">#19FFFFFF</item>
         <item name="android:colorPrimary">#FF212121</item>
         <item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
-        <item name="allappsHeaderProtectionColor">@color/material_color_surface_container_low</item>
+        <item name="allappsHeaderProtectionColor">?attr/materialColorSurfaceContainerLow</item>
         <item name="allAppsNavBarScrimColor">#80000000</item>
         <item name="popupColorPrimary">@color/popup_color_primary_dark</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
-        <item name="popupColorBackground">#1F2020</item>
-        <item name="popupTextColor">@color/popup_text_color_dark</item>
+        <item name="popupTextColor">@color/system_on_surface_dark</item>
         <item name="popupNotificationDotColor">@color/popup_notification_dot_dark</item>
         <item name="popupShadeFirst">@color/popup_shade_first_dark</item>
         <item name="popupShadeSecond">@color/popup_shade_second_dark</item>
@@ -173,7 +178,7 @@
         <item name="isFolderDarkText">false</item>
         <item name="folderTextColor">@color/folder_text_color_dark</item>
         <item name="folderHintTextColor">@color/folder_hint_text_color_dark</item>
-        <item name="appPairSurfaceInFolder">@color/material_color_surface_container_lowest</item>
+        <item name="appPairSurfaceInFolder">?attr/materialColorSurfaceContainerLowest</item>
         <item name="isMainColorDark">true</item>
         <item name="loadingIconColor">#99FFFFFF</item>
         <item name="iconOnlyShortcutColor">#B3FFFFFF</item>
@@ -244,6 +249,7 @@
             @color/widget_picker_secondary_surface_color_light</item>
         <item name="widgetPickerTitleColor">@color/widget_picker_title_color_light</item>
         <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_light</item>
+        <item name="widgetPickerWidgetOptionsMenuColor">@color/widget_picker_menu_options_color_light</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_light</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
@@ -269,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">
@@ -284,6 +296,7 @@
         <item name="widgetPickerTitleColor">
             @color/widget_picker_title_color_dark</item>
         <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_dark</item>
+        <item name="widgetPickerWidgetOptionsMenuColor">@color/widget_picker_menu_options_color_dark</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_dark</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
@@ -309,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">
@@ -463,7 +482,7 @@
 
     <style name="PrivateSpaceHeaderTextStyle">
         <item name="android:textSize">16sp</item>
-        <item name="android:textColor">@color/material_color_on_surface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
         <item name="android:fontFamily">google-sans-text-medium</item>
         <item name="android:ellipsize">end</item>
     </style>
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/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 876b643..76c0f90 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -74,7 +74,8 @@
             TYPE_TASKBAR_ALL_APPS,
             TYPE_ADD_TO_HOME_CONFIRMATION,
             TYPE_TASKBAR_OVERLAY_PROXY,
-            TYPE_TASKBAR_PINNING_POPUP
+            TYPE_TASKBAR_PINNING_POPUP,
+            TYPE_PIN_IME_POPUP
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -138,9 +139,6 @@
     public static final int TYPE_TOUCH_CONTROLLER_NO_INTERCEPT = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE
             & ~TYPE_LISTENER & ~TYPE_TASKBAR_OVERLAYS;
 
-    public static final int TYPE_ALL_EXCEPT_ON_BOARD_POPUP = TYPE_ALL & ~TYPE_ON_BOARD_POPUP
-            & ~TYPE_PIN_IME_POPUP;
-
     protected boolean mIsOpen;
 
     public AbstractFloatingView(Context context, AttributeSet attrs) {
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/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index ef56246..b51e850 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -37,8 +37,6 @@
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.debug.TestEvent;
-import com.android.launcher3.debug.TestEventEmitter;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logging.InstanceId;
@@ -223,9 +221,6 @@
         dl.addView(frame);
         frame.mIsOpen = true;
         frame.post(() -> frame.snapToWidget(false));
-        TestEventEmitter.INSTANCE.get(widget.getContext()).sendEvent(
-                TestEvent.RESIZE_FRAME_SHOWING
-        );
     }
 
     private void setCornerRadiusFromWidget() {
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 633091d..2e75261 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -110,11 +110,6 @@
     public static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 4;
 
     /**
-     * State flag indicating if the user will be active shortly.
-     */
-    public static final int ACTIVITY_STATE_USER_WILL_BE_ACTIVE = 1 << 5;
-
-    /**
      * State flag indicating that a state transition is in progress
      */
     public static final int ACTIVITY_STATE_TRANSITION_ACTIVE = 1 << 6;
@@ -193,7 +188,7 @@
 
     public SystemUiController getSystemUiController() {
         if (mSystemUiController == null) {
-            mSystemUiController = new SystemUiController(getWindow());
+            mSystemUiController = new SystemUiController(getWindow().getDecorView());
         }
         return mSystemUiController;
     }
@@ -316,7 +311,6 @@
      */
     public void setResumed() {
         addActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE);
-        removeActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
     }
 
     public boolean isUserActive() {
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 8585b66..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();
         }
@@ -140,15 +140,11 @@
     }
 
     protected void onDeviceProfileInitiated() {
-        if (mDeviceProfile.isVerticalBarLayout()) {
-            mDeviceProfile.updateIsSeascape(this);
-        }
     }
 
     @Override
     public void onDisplayInfoChanged(Context context, Info info, int flags) {
         if ((flags & CHANGE_ROTATION) != 0 && mDeviceProfile.isVerticalBarLayout()) {
-            mDeviceProfile.updateIsSeascape(this);
             reapplyUi();
         }
     }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 83427a0..ef5c88a 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,8 +16,11 @@
 
 package com.android.launcher3;
 
+import static android.graphics.fonts.FontStyle.FONT_WEIGHT_BOLD;
+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;
@@ -37,26 +40,33 @@
 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;
+import android.text.Spannable;
+import android.text.SpannableString;
 import android.text.StaticLayout;
 import android.text.TextPaint;
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
+import android.text.style.ImageSpan;
 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;
@@ -82,7 +92,7 @@
 import com.android.launcher3.util.ShortcutUtil;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.views.IconLabelDotView;
+import com.android.launcher3.views.FloatingIconViewCompanion;
 
 import java.text.NumberFormat;
 import java.util.HashMap;
@@ -94,7 +104,9 @@
  * too aggressive.
  */
 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
-        IconLabelDotView, DraggableView, Reorderable {
+        FloatingIconViewCompanion, DraggableView, Reorderable {
+
+    public static final String TAG = "BubbleTextView";
 
     public static final int DISPLAY_WORKSPACE = 0;
     public static final int DISPLAY_ALL_APPS = 1;
@@ -111,6 +123,7 @@
     private static final String EMPTY = "";
     private static final StringMatcherUtility.StringMatcher MATCHER =
             StringMatcherUtility.StringMatcher.getInstance();
+    private static final int BOLD_TEXT_ADJUSTMENT = FONT_WEIGHT_BOLD - FONT_WEIGHT_NORMAL;
 
     private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
 
@@ -166,7 +179,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mSkipUserBadge = false;
     @ViewDebug.ExportedProperty(category = "launcher")
-    private boolean mIsIconVisible = true;
+    protected boolean mIsIconVisible = true;
     @ViewDebug.ExportedProperty(category = "launcher")
     private int mTextColor;
     @ViewDebug.ExportedProperty(category = "launcher")
@@ -177,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.
@@ -210,7 +225,7 @@
 
     private CancellableTask mIconLoadRequest;
 
-    private boolean mEnableIconUpdateAnimation = false;
+    private boolean mHighResUpdateInProgress = false;
 
     public BubbleTextView(Context context) {
         this(context, null, 0);
@@ -267,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);
@@ -484,7 +498,7 @@
     }
 
     protected boolean isCurrentLanguageEnglish() {
-        return mCurrentLocale.equals(Locale.US);
+        return mCurrentLanguage.equals(Locale.ENGLISH.getLanguage());
     }
 
     @UiThread
@@ -494,7 +508,13 @@
             mLastOriginalText = label;
             mLastModifiedText = mLastOriginalText;
             mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER);
-            setText(label);
+            if (Flags.useNewIconForArchivedApps()
+                    && info instanceof ItemInfoWithIcon infoWithIcon
+                    && infoWithIcon.isInactiveArchive()) {
+                setTextWithArchivingIcon(label);
+            } else {
+                setText(label);
+            }
         }
         if (info.contentDescription != null) {
             setContentDescription(info.isDisabled()
@@ -503,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) {
@@ -694,22 +724,55 @@
         }
     }
 
+    /** Draws a background behind the App Title label when required. **/
+    public void drawAppContrastTile(Canvas canvas) {
+        RectF appTitleBounds;
+        Paint.FontMetrics fm = getPaint().getFontMetrics();
+        Rect tmpRect = new Rect();
+        getDrawingRect(tmpRect);
+
+        if (mIcon == null) {
+            appTitleBounds = new RectF(0, 0, tmpRect.right,
+                    (int) Math.ceil(fm.bottom - fm.top));
+        } else {
+            Rect iconBounds = new Rect();
+            getIconBounds(iconBounds);
+            int textStart = iconBounds.bottom + getCompoundDrawablePadding();
+            appTitleBounds = new RectF(tmpRect.left, textStart, tmpRect.right,
+                    textStart + (int) Math.ceil(fm.bottom - fm.top));
+        }
+
+        canvas.drawRoundRect(appTitleBounds, appTitleBounds.height() / 2,
+                appTitleBounds.height() / 2,
+                PillColorProvider.getInstance(getContext()).getAppTitlePillPaint());
+    }
+
     /** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
     protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
         if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
             return;
         }
         getIconBounds(mRunningAppIconBounds);
-        // TODO(b/333872717): update color, shape, and size of indicator
-        boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
-        int indicatorTop =
-                mRunningAppIconBounds.bottom + (isMinimized ? mMinimizedAppIndicatorTopMargin
-                        : mRunningAppIndicatorTopMargin);
-        final Size indicatorSize =
-                isMinimized ? mMinimizedAppIndicatorSize : mRunningAppIndicatorSize;
-        canvas.drawRect(mRunningAppIconBounds.centerX() - indicatorSize.getWidth() / 2,
-                indicatorTop, mRunningAppIconBounds.centerX() + indicatorSize.getWidth() / 2,
-                indicatorTop + indicatorSize.getHeight(), mRunningAppIndicatorPaint);
+        Utilities.scaleRectAboutCenter(
+                mRunningAppIconBounds,
+                IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+
+        final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
+        final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
+        final int indicatorWidth =
+                isMinimized ? mMinimizedAppIndicatorWidth : mRunningAppIndicatorWidth;
+        final float cornerRadius = mRunningAppIndicatorHeight / 2f;
+        mRunningAppIndicatorPaint.setColor(
+                isMinimized ? mMinimizedAppIndicatorColor : mRunningAppIndicatorColor);
+
+        canvas.drawRoundRect(
+                mRunningAppIconBounds.centerX() - indicatorWidth / 2f,
+                indicatorTop,
+                mRunningAppIconBounds.centerX() + indicatorWidth / 2f,
+                indicatorTop + mRunningAppIndicatorHeight,
+                cornerRadius,
+                cornerRadius,
+                mRunningAppIndicatorPaint);
     }
 
     @Override
@@ -804,7 +867,13 @@
                     getLineSpacingExtra());
             if (!TextUtils.equals(modifiedString, mLastModifiedText)) {
                 mLastModifiedText = modifiedString;
-                setText(modifiedString);
+                if (Flags.useNewIconForArchivedApps()
+                        && getTag() instanceof ItemInfoWithIcon infoWithIcon
+                        && infoWithIcon.isInactiveArchive()) {
+                    setTextWithArchivingIcon(modifiedString);
+                } else {
+                    setText(modifiedString);
+                }
                 // if text contains NEW_LINE, set max lines to 2
                 if (TextUtils.indexOf(modifiedString, NEW_LINE) != -1) {
                     setSingleLine(false);
@@ -825,9 +894,49 @@
         super.setTextColor(getModifiedColor());
     }
 
+    /**
+     * Sets text with a start icon for App Archiving.
+     * Uses a bolded drawable if text is bolded.
+     * @param text
+     */
+    private void setTextWithArchivingIcon(CharSequence text) {
+        var drawableId = R.drawable.cloud_download_24px;
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
+                && getResources().getConfiguration().fontWeightAdjustment >= BOLD_TEXT_ADJUSTMENT) {
+            // If System bold text setting is on, then use a bolded icon
+            drawableId = R.drawable.cloud_download_semibold_24px;
+        }
+        setTextWithStartIcon(text, drawableId);
+    }
+
+    /**
+     * Uses a SpannableString to set text with a Drawable at the start of the TextView
+     * @param text text to use for TextView
+     * @param drawableId Drawable Resource to use for drawing image at start of text
+     */
+    @VisibleForTesting
+    public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) {
+        Drawable drawable = getContext().getDrawable(drawableId);
+        if (drawable == null) {
+            setText(text);
+            Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
+                    + ", will just set text instead.");
+            return;
+        }
+        drawable.setTint(getCurrentTextColor());
+        drawable.setBounds(0, 0, Math.round(getTextSize()), Math.round(getTextSize()));
+        ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER);
+        // First space will be replaced with Drawable, second space is for space before text.
+        SpannableString spannable = new SpannableString("  " + text);
+        spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+        setText(spannable);
+    }
+
     @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);
@@ -844,6 +953,15 @@
                 && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
     }
 
+    /**
+     * Whether or not an App title contrast tile should be drawn for this element.
+     **/
+    public boolean shouldDrawAppContrastTile() {
+        return mDisplay == DISPLAY_WORKSPACE && shouldTextBeVisible()
+                && PillColorProvider.getInstance(getContext()).isMatchaEnabled()
+                && enableContrastTiles();
+    }
+
     public void setTextVisibility(boolean visible) {
         setTextAlpha(visible ? 1 : 0);
     }
@@ -983,12 +1101,11 @@
     /** Applies the given progress level to the this icon's progress bar. */
     @Nullable
     public PreloadIconDrawable applyProgressLevel() {
-        if (!(getTag() instanceof ItemInfoWithIcon)
+        if (!(getTag() instanceof ItemInfoWithIcon info)
                 || ((ItemInfoWithIcon) getTag()).isInactiveArchive()) {
             return null;
         }
 
-        ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
         int progressLevel = info.getProgressLevel();
         if (progressLevel >= 100) {
             setContentDescription(info.contentDescription != null
@@ -1008,6 +1125,10 @@
             } else {
                 preloadIconDrawable = makePreloadIcon();
                 setIcon(preloadIconDrawable);
+                if (info.isArchived() && Flags.useNewIconForArchivedApps()) {
+                    // reapply text without cloud icon as soon as unarchiving is triggered
+                    applyLabel(info);
+                }
             }
             return preloadIconDrawable;
         }
@@ -1067,6 +1188,9 @@
                 if (itemInfo.isDisabled()) {
                     setContentDescription(getContext().getString(R.string.disabled_app_label,
                             itemInfo.contentDescription));
+                } else if (itemInfo instanceof WorkspaceItemInfo wai && wai.isArchived()) {
+                    setContentDescription(
+                            getContext().getString(R.string.app_archived_title, itemInfo.title));
                 } else if (hasDot()) {
                     int count = mDotInfo.getNotificationCount();
                     setContentDescription(
@@ -1079,8 +1203,16 @@
     }
 
     private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
-        if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_ARCHIVED) != 0 && progressLevel == 0) {
-            setContentDescription(getContext().getString(R.string.app_archived_title, info.title));
+        if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_ARCHIVED) != 0
+                && progressLevel == 0) {
+            if (mIcon instanceof PreloadIconDrawable) {
+                // Tell user that download is pending and not to tap to download again.
+                setContentDescription(getContext().getString(
+                        R.string.app_waiting_download_title, info.title));
+            } else {
+                setContentDescription(getContext().getString(
+                        R.string.app_archived_title, info.title));
+            }
         } else if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
                 != 0) {
             String percentageString = NumberFormat.getPercentInstance()
@@ -1132,10 +1264,6 @@
         }
     }
 
-    protected boolean iconUpdateAnimationEnabled() {
-        return mEnableIconUpdateAnimation;
-    }
-
     protected void applyCompoundDrawables(Drawable icon) {
         if (icon == null) {
             // Icon can be null when we use the BubbleTextView for text only.
@@ -1153,7 +1281,7 @@
         // If the current icon is a placeholder color, animate its update.
         if (mIcon != null
                 && mIcon instanceof PlaceHolderIconDrawable
-                && iconUpdateAnimationEnabled()) {
+                && mHighResUpdateInProgress) {
             ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
         }
 
@@ -1175,7 +1303,7 @@
         if (getTag() == info) {
             mIconLoadRequest = null;
             mDisableRelayout = true;
-            mEnableIconUpdateAnimation = true;
+            mHighResUpdateInProgress = true;
 
             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
             info.bitmap.icon.prepareToDraw();
@@ -1190,7 +1318,7 @@
             }
 
             mDisableRelayout = false;
-            mEnableIconUpdateAnimation = false;
+            mHighResUpdateInProgress = false;
         }
     }
 
@@ -1202,7 +1330,7 @@
             mIconLoadRequest.cancel();
             mIconLoadRequest = null;
         }
-        if (getTag() instanceof ItemInfoWithIcon) {
+        if (getTag() instanceof ItemInfoWithIcon && !mHighResUpdateInProgress) {
             ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
             if (info.usingLowResIcon()) {
                 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 7e9e864..ee72c22 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -86,7 +86,7 @@
 
 public class CellLayout extends ViewGroup {
     private static final String TAG = "CellLayout";
-    private static final boolean LOGD = false;
+    private static final boolean LOGD = true;
 
     /** The color of the "leave-behind" shape when a folder is opened from Hotseat. */
     private static final int FOLDER_LEAVE_BEHIND_COLOR = Color.argb(160, 245, 245, 245);
@@ -166,6 +166,7 @@
     private final int[] mDragCellSpan = new int[2];
 
     private boolean mDragging = false;
+    public boolean mHasOnLayoutBeenCalled = false;
 
     private final TimeInterpolator mEaseOutInterpolator;
     protected final ShortcutAndWidgetContainer mShortcutsAndWidgets;
@@ -1009,6 +1010,7 @@
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        mHasOnLayoutBeenCalled = true; // b/349929393 - is the required call to onLayout not done?
         int left = getPaddingLeft();
         left += (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
         int right = r - l - getPaddingRight();
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 00db3a3..78535a1 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -24,14 +24,16 @@
 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.config.FeatureFlags.ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.icons.GraphicsUtils.getShapePath;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 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;
@@ -221,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
@@ -235,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;
@@ -298,9 +301,6 @@
     // the widgetView, such that the actual view size is same as the widget size.
     public final Rect widgetPadding = new Rect();
 
-    // When true, nav bar is on the left side of the screen.
-    private boolean mIsSeascape;
-
     // Notification dots
     public final DotRenderer mDotRendererWorkSpace;
     public final DotRenderer mDotRendererAllApps;
@@ -320,6 +320,74 @@
     // DragController
     public int flingToDeleteThresholdVelocity;
 
+    /** Used only as an alternative to mocking when null values cannot be used. */
+    @VisibleForTesting
+    public DeviceProfile() {
+        inv = null;
+        mInfo = null;
+        mMetrics = null;
+        mIconSizeSteps = null;
+        isTablet = false;
+        isPhone = false;
+        transposeLayoutWithOrientation = false;
+        isMultiDisplay = false;
+        isTwoPanels = false;
+        isPredictiveBackSwipe = false;
+        isQsbInline = false;
+        isLandscape = false;
+        isMultiWindowMode = false;
+        isGestureMode = false;
+        isLeftRightSplit = false;
+        windowX = 0;
+        windowY = 0;
+        widthPx = 0;
+        heightPx = 0;
+        availableWidthPx = 0;
+        availableHeightPx = 0;
+        rotationHint = 0;
+        aspectRatio = 1;
+        mIsScalableGrid = false;
+        mTypeIndex = 0;
+        mIsResponsiveGrid = false;
+        desiredWorkspaceHorizontalMarginOriginalPx = 0;
+        edgeMarginPx = 0;
+        workspaceContentScale = 0;
+        workspaceSpringLoadedMinNextPageVisiblePx = 0;
+        extraSpace = 0;
+        workspacePageIndicatorHeight = 0;
+        mWorkspacePageIndicatorOverlapWorkspace = 0;
+        numFolderRows = 0;
+        numFolderColumns = 0;
+        folderLabelTextScale = 0;
+        areNavButtonsInline = false;
+        mHotseatBarEdgePaddingPx = 0;
+        mHotseatBarWorkspaceSpacePx = 0;
+        hotseatQsbWidth = 0;
+        hotseatQsbHeight = 0;
+        hotseatQsbVisualHeight = 0;
+        hotseatQsbShadowHeight = 0;
+        hotseatBorderSpace = 0;
+        mMinHotseatIconSpacePx = 0;
+        mMinHotseatQsbWidthPx = 0;
+        mMaxHotseatIconSpacePx = 0;
+        inlineNavButtonsEndSpacingPx = 0;
+        mBubbleBarSpaceThresholdPx = 0;
+        numShownAllAppsColumns = 0;
+        overviewActionsHeight = 0;
+        overviewActionsTopMarginPx = 0;
+        overviewActionsButtonSpacing = 0;
+        mViewScaleProvider = null;
+        mDotRendererWorkSpace = null;
+        mDotRendererAllApps = null;
+        taskbarHeight = 0;
+        stashedTaskbarHeight = 0;
+        taskbarBottomMargin = 0;
+        taskbarIconSize = 0;
+        mTransientTaskbarClaimedSpace = 0;
+        startAlignTaskbar = false;
+        isTransientTaskbar = false;
+    }
+
     /** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */
     DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds,
             SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
@@ -439,7 +507,7 @@
         bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration);
         if (isTablet) {
             bottomSheetWorkspaceScale = workspaceContentScale;
-            if (isMultiDisplay && !ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH.get()) {
+            if (isMultiDisplay) {
                 // TODO(b/259893832): Revert to use maxWallpaperScale to calculate bottomSheetDepth
                 // when screen recorder bug is fixed.
                 if (enableScalingRevealHomeAnimation()) {
@@ -549,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 =
@@ -629,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 =
@@ -764,7 +827,7 @@
             hotseatBorderSpace = cellLayoutBorderSpacePx.y;
         }
 
-        if (isTablet) {
+        if (shouldShowAllAppsOnSheet()) {
             allAppsPadding.top = mInsets.top;
             allAppsShiftRange = heightPx;
         } else {
@@ -787,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);
@@ -1282,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);
@@ -1447,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
@@ -1767,19 +1859,14 @@
      * Returns the new border space that should be used between hotseat icons after adjusting it to
      * the bubble bar.
      *
+     * <p>Does not check for visible bubbles persistence, so caller should call
+     * {@link #shouldAdjustHotseatForBubbleBar} first.
+     *
      * <p>If there's no adjustment needed, this method returns {@code 0}.
+     * @see #shouldAdjustHotseatForBubbleBar(Context, boolean)
      */
     public float getHotseatAdjustedBorderSpaceForBubbleBar(Context context) {
-        // only need to adjust when QSB is on top of the hotseat.
-        if (isQsbInline) {
-            return 0;
-        }
-
-        // no need to adjust if there's enough space for the bubble bar to the right of the hotseat.
-        if (getHotseatLayoutPadding(context).right > mBubbleBarSpaceThresholdPx) {
-            return 0;
-        }
-
+        if (!shouldAdjustHotseatForBubbleBar(context)) return 0;
         // The adjustment is shrinking the hotseat's width by 1 icon on either side.
         int iconsWidth =
                 iconSizePx * numShownHotseatIcons + hotseatBorderSpace * (numShownHotseatIcons - 1);
@@ -1789,6 +1876,33 @@
     }
 
     /**
+     * Returns the hotseat icon translation X for the cellX index.
+     *
+     * <p>Does not check for visible bubbles persistence, so caller should call
+     * {@link #shouldAdjustHotseatForBubbleBar} first.
+     *
+     * <p>If there's no adjustment needed, this method returns {@code 0}.
+     * @see #shouldAdjustHotseatForBubbleBar(Context, boolean)
+     */
+    public float getHotseatAdjustedTranslation(Context context, int cellX) {
+        if (!shouldAdjustHotseatForBubbleBar(context)) return 0;
+        float borderSpace = getHotseatAdjustedBorderSpaceForBubbleBar(context);
+        float borderSpaceDelta = borderSpace - hotseatBorderSpace;
+        return iconSizePx + cellX * borderSpaceDelta;
+    }
+
+    /** Returns whether hotseat should be adjusted for the bubble bar. */
+    public boolean shouldAdjustHotseatForBubbleBar(Context context, boolean hasBubbles) {
+        return hasBubbles && shouldAdjustHotseatForBubbleBar(context);
+    }
+
+    private boolean shouldAdjustHotseatForBubbleBar(Context context) {
+        // only need to adjust if bubble bar is enabled, when QSB is on top of the hotseat and
+        // there's not enough space for the bubble bar to the right of the hotseat.
+        return !isQsbInline && getHotseatLayoutPadding(context).right <= mBubbleBarSpaceThresholdPx;
+    }
+
+    /**
      * Returns the padding for hotseat view
      */
     public Rect getHotseatLayoutPadding(Context context) {
@@ -1807,7 +1921,7 @@
                 hotseatBarPadding.set(mHotseatBarWorkspaceSpacePx, paddingTop,
                         mInsets.right + mHotseatBarEdgePaddingPx, paddingBottom);
             }
-        } else if (isTaskbarPresent) {
+        } else if (isTaskbarPresent || inv.isFixedLandscapeMode) {
             // Center the QSB vertically with hotseat
             int hotseatBarBottomPadding = getHotseatBarBottomPadding();
             int hotseatBarTopPadding =
@@ -1826,6 +1940,13 @@
             }
             startSpacing += getAdditionalQsbSpace();
 
+            if (inv.isFixedLandscapeMode) {
+                endSpacing += workspacePadding.right + cellLayoutPaddingPx.right
+                        + mInsets.right;
+                startSpacing += workspacePadding.left + cellLayoutPaddingPx.left
+                        + mInsets.left;
+            }
+
             hotseatBarPadding.top = hotseatBarTopPadding;
             hotseatBarPadding.bottom = hotseatBarBottomPadding;
             boolean isRtl = Utilities.isRtl(context.getResources());
@@ -1935,6 +2056,18 @@
     }
 
     /**
+     * Returns the number of pixels the hotseat icons vertical center is translated from the bottom
+     * of the screen.
+     */
+    public int getHotseatVerticalCenter() {
+        return hotseatBarSizePx
+                - (isQsbInline ? 0 : hotseatQsbVisualHeight)
+                - hotseatQsbSpace
+                - (hotseatCellHeightPx / 2)
+                + ((hotseatCellHeightPx - iconSizePx) / 2);
+    }
+
+    /**
      * Returns the number of pixels the taskbar is translated from the bottom of the screen.
      */
     public int getTaskbarOffsetY() {
@@ -2008,25 +2141,8 @@
         return isLandscape && transposeLayoutWithOrientation;
     }
 
-    /**
-     * Updates orientation information and returns true if it has changed from the previous value.
-     */
-    public boolean updateIsSeascape(Context context) {
-        if (isVerticalBarLayout()) {
-            boolean isSeascape = DisplayController.INSTANCE.get(context)
-                    .getInfo().rotation == Surface.ROTATION_270;
-            if (mIsSeascape != isSeascape) {
-                mIsSeascape = isSeascape;
-                // Hotseat changing sides requires updating workspace left/right paddings
-                updateWorkspacePadding();
-                return true;
-            }
-        }
-        return false;
-    }
-
     public boolean isSeascape() {
-        return isVerticalBarLayout() && mIsSeascape;
+        return rotationHint == Surface.ROTATION_270 && isVerticalBarLayout();
     }
 
     public boolean shouldFadeAdjacentWorkspaceScreens() {
@@ -2168,6 +2284,10 @@
                 mHotseatBarEdgePaddingPx));
         writer.println(prefix + pxToDpStr("mHotseatBarWorkspaceSpacePx",
                 mHotseatBarWorkspaceSpacePx));
+        writer.println(prefix
+                + pxToDpStr("inlineNavButtonsEndSpacingPx", inlineNavButtonsEndSpacingPx));
+        writer.println(prefix
+                + pxToDpStr("navButtonsLayoutWidthPx", navButtonsLayoutWidthPx));
         writer.println(prefix + pxToDpStr("hotseatBarEndOffset", hotseatBarEndOffset));
         writer.println(prefix + pxToDpStr("hotseatQsbSpace", hotseatQsbSpace));
         writer.println(prefix + pxToDpStr("hotseatQsbHeight", hotseatQsbHeight));
@@ -2282,6 +2402,29 @@
     }
 
     /**
+     * Returns whether Taskbar and Hotseat should adjust horizontally on bubble bar location update.
+     */
+    public boolean shouldAdjustHotseatOnNavBarLocationUpdate(Context context) {
+        return enableBubbleBar()
+                && enableBubbleBarInPersistentTaskBar()
+                && !DisplayController.getNavigationMode(context).hasGestures;
+    }
+
+    /** Returns hotseat translation X for the bubble bar position. */
+    public int getHotseatTranslationXForNavBar(Context context, boolean isBubblesOnLeft) {
+        if (shouldAdjustHotseatOnNavBarLocationUpdate(context)) {
+            boolean isRtl = Utilities.isRtl(context.getResources());
+            if (isBubblesOnLeft) {
+                return isRtl ? -navButtonsLayoutWidthPx : 0;
+            } else {
+                return isRtl ? 0 : navButtonsLayoutWidthPx;
+            }
+        } else {
+            return 0;
+        }
+    }
+
+    /**
      * Callback when a component changes the DeviceProfile associated with it, as a result of
      * configuration change
      */
@@ -2399,7 +2542,8 @@
                 throw new IllegalArgumentException("Window bounds not set");
             }
             if (mTransposeLayoutWithOrientation == null) {
-                mTransposeLayoutWithOrientation = !mInfo.isTablet(mWindowBounds);
+                mTransposeLayoutWithOrientation =
+                        !(mInfo.isTablet(mWindowBounds) || mInv.isFixedLandscapeMode);
             }
             if (mIsGestureMode == null) {
                 mIsGestureMode = mInfo.getNavigationMode().hasGestures;
diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt
index e022159..f1029b1 100644
--- a/src/com/android/launcher3/DropTargetHandler.kt
+++ b/src/com/android/launcher3/DropTargetHandler.kt
@@ -35,8 +35,7 @@
                     target?.let {
                         deferred.mPackageName = it.packageName
                         mLauncher.addEventCallback(EVENT_RESUMED) { deferred.onLauncherResume() }
-                    }
-                        ?: deferred.sendFailure()
+                    } ?: deferred.sendFailure()
                 }
             }
         }
@@ -47,19 +46,10 @@
         mLauncher.appWidgetHolder.startConfigActivity(
             mLauncher,
             widgetId,
-            ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET
+            ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET,
         )
     }
 
-    fun dismissPrediction(
-        announcement: CharSequence,
-        onActionClicked: Runnable,
-        onDismiss: Runnable?
-    ) {
-        mLauncher.dragLayer.announceForAccessibility(announcement)
-        Snackbar.show(mLauncher, R.string.item_removed, R.string.undo, onDismiss, onActionClicked)
-    }
-
     fun getViewUnderDrag(info: ItemInfo): View? {
         return if (
             info is LauncherAppWidgetInfo &&
@@ -95,7 +85,7 @@
             R.string.item_removed,
             R.string.undo,
             mLauncher.modelWriter::commitDelete,
-            onUndoClicked
+            onUndoClicked,
         )
     }
 
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index 960d77a..17084bb 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -20,16 +20,15 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 
@@ -56,9 +55,12 @@
         super(context, attrs, defStyleAttr);
     }
 
-    public void bindFastScrollbar(RecyclerViewFastScroller scrollbar) {
+    public void bindFastScrollbar(RecyclerViewFastScroller scrollbar,
+            RecyclerViewFastScroller.FastScrollerLocation location) {
         mScrollbar = scrollbar;
         mScrollbar.setRecyclerView(this);
+        mScrollbar.setFastScrollerLocation(location);
+        scrollToTop();
         onUpdateScrollbar(0);
     }
 
@@ -165,6 +167,13 @@
     public abstract void onUpdateScrollbar(int dy);
 
     /**
+     * Return the fast scroll letter list view in the A-Z list.
+     */
+    public ConstraintLayout getLetterList() {
+        return null;
+    }
+
+    /**
      * <p>Override in each subclass of this base class.
      */
     public void onFastScrollCompleted() {}
@@ -189,10 +198,6 @@
      * Scrolls this recycler view to the top.
      */
     public void scrollToTop() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PRIVATE_SPACE_SCROLL_FAILURE, "FastScrollRecyclerView#scrollToTop",
-                    new Exception());
-        }
         if (mScrollbar != null) {
             mScrollbar.reattachThumbToScroll();
         }
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index f775673..6468f74 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -33,15 +33,44 @@
 import android.view.ViewGroup;
 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;
 import com.android.launcher3.views.ActivityContext;
 
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * View class that represents the bottom row of the home screen.
  */
 public class Hotseat extends CellLayout implements Insettable {
 
+    public static final int ALPHA_CHANNEL_TASKBAR_ALIGNMENT = 0;
+    public static final int ALPHA_CHANNEL_PREVIEW_RENDERER = 1;
+    public static final int ALPHA_CHANNEL_TASKBAR_STASH = 2;
+    public static final int ALPHA_CHANNEL_CHANNELS_COUNT = 3;
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @IntDef({ALPHA_CHANNEL_TASKBAR_ALIGNMENT, ALPHA_CHANNEL_PREVIEW_RENDERER,
+            ALPHA_CHANNEL_TASKBAR_STASH})
+    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;
@@ -50,6 +79,12 @@
     private boolean mHasVerticalHotseat;
     private Workspace<?> mWorkspace;
     private boolean mSendTouchToWorkspace;
+    private final MultiValueAlpha mIconsAlphaChannels;
+    private final MultiValueAlpha mQsbAlphaChannels;
+
+    private @Nullable MultiProperty mQsbTranslationX;
+
+    private final MultiPropertyFactory mIconsTranslationXFactory;
 
     private final View mQsb;
 
@@ -63,9 +98,28 @@
 
     public Hotseat(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-
         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
         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;
     }
 
     /**
@@ -95,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;
@@ -136,24 +187,22 @@
     public void adjustForBubbleBar(boolean isBubbleBarVisible) {
         DeviceProfile dp = mActivity.getDeviceProfile();
         float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
-        if (Float.compare(adjustedBorderSpace, 0f) == 0) {
-            return;
-        }
+        boolean adjustmentRequired = Float.compare(adjustedBorderSpace, 0f) != 0;
 
         ShortcutAndWidgetContainer icons = getShortcutsAndWidgets();
-        AnimatorSet animatorSet = new AnimatorSet();
-        float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace;
-
         // update the translation provider for future layout passes of hotseat icons.
-        if (isBubbleBarVisible) {
-            icons.setTranslationProvider(cellX -> dp.iconSizePx + cellX * borderSpaceDelta);
+        if (adjustmentRequired && isBubbleBarVisible) {
+            icons.setTranslationProvider(
+                    cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX));
         } else {
             icons.setTranslationProvider(null);
         }
+        if (!adjustmentRequired) return;
 
+        AnimatorSet animatorSet = new AnimatorSet();
         for (int i = 0; i < icons.getChildCount(); i++) {
             View child = icons.getChildAt(i);
-            float tx = isBubbleBarVisible ? dp.iconSizePx + i * borderSpaceDelta : 0;
+            float tx = isBubbleBarVisible ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0;
             if (child instanceof Reorderable) {
                 MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate();
                 animatorSet.play(
@@ -162,14 +211,14 @@
                 animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx));
             }
         }
-        if (mQsb instanceof HorizontalInsettableView) {
-            HorizontalInsettableView horizontalInsettableQsb = (HorizontalInsettableView) mQsb;
-            ValueAnimator qsbAnimator = ValueAnimator.ofFloat(0f, 1f);
+        if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) {
+            final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets();
+            final float targetInsetFraction =
+                    isBubbleBarVisible ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0;
+            ValueAnimator qsbAnimator =
+                    ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction);
             qsbAnimator.addUpdateListener(animation -> {
-                float fraction = qsbAnimator.getAnimatedFraction();
-                float insetFraction = isBubbleBarVisible
-                        ? (float) dp.iconSizePx * fraction / dp.hotseatQsbWidth
-                        : (float) dp.iconSizePx * (1 - fraction) / dp.hotseatQsbWidth;
+                float insetFraction = (float) animation.getAnimatedValue();
                 horizontalInsettableQsb.setHorizontalInsets(insetFraction);
             });
             animatorSet.play(qsbAnimator);
@@ -270,21 +319,27 @@
     }
 
     /**
-     * Sets the alpha value of just our ShortcutAndWidgetContainer.
+     * Sets the alpha value of the specified alpha channel of just our ShortcutAndWidgetContainer.
      */
-    public void setIconsAlpha(float alpha) {
-        getShortcutsAndWidgets().setAlpha(alpha);
+    public void setIconsAlpha(float alpha, @HotseatQsbAlphaId int channelId) {
+        getIconsAlpha(channelId).setValue(alpha);
     }
 
     /**
      * Sets the alpha value of just our QSB.
      */
-    public void setQsbAlpha(float alpha) {
-        mQsb.setAlpha(alpha);
+    public void setQsbAlpha(float alpha, @HotseatQsbAlphaId int channelId) {
+        getQsbAlpha(channelId).setValue(alpha);
     }
 
-    public float getIconsAlpha() {
-        return getShortcutsAndWidgets().getAlpha();
+    /** Returns the alpha channel for ShortcutAndWidgetContainer */
+    public MultiProperty getIconsAlpha(@HotseatQsbAlphaId int channelId) {
+        return mIconsAlphaChannels.get(channelId);
+    }
+
+    /** Returns the alpha channel for Qsb */
+    public MultiProperty getQsbAlpha(@HotseatQsbAlphaId int channelId) {
+        return mQsbAlphaChannels.get(channelId);
     }
 
     /**
@@ -294,4 +349,24 @@
         return mQsb;
     }
 
+    /** Dumps the Hotseat internal state */
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "Hotseat:");
+        mIconsAlphaChannels.dump(
+                prefix + "\t",
+                writer,
+                "mIconsAlphaChannels",
+                "ALPHA_CHANNEL_TASKBAR_ALIGNMENT",
+                "ALPHA_CHANNEL_PREVIEW_RENDERER",
+                "ALPHA_CHANNEL_TASKBAR_STASH");
+        mQsbAlphaChannels.dump(
+                prefix + "\t",
+                writer,
+                "mQsbAlphaChannels",
+                "ALPHA_CHANNEL_TASKBAR_ALIGNMENT",
+                "ALPHA_CHANNEL_PREVIEW_RENDERER",
+                "ALPHA_CHANNEL_TASKBAR_STASH"
+        );
+    }
+
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 54aea38..04e4b57 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherPrefs.FIXED_LANDSCAPE_MODE;
 import static com.android.launcher3.LauncherPrefs.GRID_NAME;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
@@ -34,6 +35,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Trace;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
@@ -51,15 +53,14 @@
 
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DeviceGridState;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Partner;
+import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -86,7 +87,8 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({TYPE_PHONE, TYPE_MULTI_DISPLAY, TYPE_TABLET})
-    public @interface DeviceType {}
+    public @interface DeviceType {
+    }
 
     public static final int TYPE_PHONE = 0;
     public static final int TYPE_MULTI_DISPLAY = 1;
@@ -132,6 +134,7 @@
     public int iconBitmapSize;
     public int fillResIconDpi;
     public @DeviceType int deviceType;
+    public Info displayInfo;
 
     public PointF[] minCellSize;
 
@@ -185,6 +188,8 @@
     @XmlRes
     public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
     @XmlRes
+    public int rowCountSpecsId = INVALID_RESOURCE_HANDLE;;
+    @XmlRes
     public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
     @XmlRes
     public int allAppsSpecsId = INVALID_RESOURCE_HANDLE;
@@ -207,6 +212,13 @@
     @XmlRes
     public int allAppsCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
 
+
+    /**
+     * Fixed landscape mode is the landscape on the phones.
+     */
+    public boolean isFixedLandscapeMode = false;
+    private LauncherPrefChangeListener mLandscapeModePreferenceListener;
+
     public String dbFile;
     public int defaultLayoutId;
     public int demoModeLayoutId;
@@ -222,7 +234,8 @@
     private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
 
     @VisibleForTesting
-    public InvariantDeviceProfile() { }
+    public InvariantDeviceProfile() {
+    }
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
@@ -231,8 +244,6 @@
         if (!newGridName.equals(gridName)) {
             LauncherPrefs.get(context).put(GRID_NAME, newGridName);
         }
-        LockedUserState.get(context).runOnUserUnlocked(() ->
-            new DeviceGridState(this).writeToPrefs(context));
 
         DisplayController.INSTANCE.get(context).setPriorityListener(
                 (displayContext, info, flags) -> {
@@ -242,6 +253,18 @@
                         onConfigChanged(displayContext);
                     }
                 });
+        if (Flags.oneGridSpecs()) {
+            mLandscapeModePreferenceListener = (String s) -> {
+                boolean newFixedLandscapeValue = FIXED_LANDSCAPE_MODE.get(context);
+                if (isFixedLandscapeMode != newFixedLandscapeValue) {
+                    setFixedLandscape(context, newFixedLandscapeValue);
+                }
+            };
+            LauncherPrefs.INSTANCE.get(context).addListener(
+                    mLandscapeModePreferenceListener,
+                    FIXED_LANDSCAPE_MODE
+            );
+        }
     }
 
     /**
@@ -267,8 +290,13 @@
         @DeviceType int defaultDeviceType = defaultInfo.getDeviceType();
         DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
                 defaultInfo,
-                getPredefinedDeviceProfiles(context, gridName, defaultDeviceType,
-                        /*allowDisabledGrid=*/false),
+                getPredefinedDeviceProfiles(
+                        context,
+                        gridName,
+                        defaultInfo,
+                        /*allowDisabledGrid=*/false,
+                        isFixedLandscapeMode
+                ),
                 defaultDeviceType);
 
         Context displayContext = context.createDisplayContext(display);
@@ -276,8 +304,13 @@
         @DeviceType int deviceType = myInfo.getDeviceType();
         DisplayOption myDisplayOption = invDistWeightedInterpolate(
                 myInfo,
-                getPredefinedDeviceProfiles(context, gridName, deviceType,
-                        /*allowDisabledGrid=*/false),
+                getPredefinedDeviceProfiles(
+                        context,
+                        gridName,
+                        myInfo,
+                        /*allowDisabledGrid=*/false,
+                        isFixedLandscapeMode
+                ),
                 deviceType);
 
         DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
@@ -294,40 +327,16 @@
         System.arraycopy(defaultDisplayOption.borderSpaces, 0, result.borderSpaces, 0,
                 COUNT_SIZES);
 
-        initGrid(context, myInfo, result, deviceType);
+        initGrid(context, myInfo, result);
     }
 
     @Override
     public void close() {
         DisplayController.INSTANCE.executeIfCreated(dc -> dc.setPriorityListener(null));
-    }
-
-    /**
-     * Reinitialize the current grid after a restore, where some grids might now be disabled.
-     */
-    public void reinitializeAfterRestore(Context context) {
-        String currentGridName = getCurrentGridName(context);
-        String currentDbFile = dbFile;
-        String newGridName = initGrid(context, currentGridName);
-        String newDbFile = dbFile;
-        FileLog.d(TAG, "Reinitializing grid after restore."
-                + " currentGridName=" + currentGridName
-                + ", currentDbFile=" + currentDbFile
-                + ", newGridName=" + newGridName
-                + ", newDbFile=" + newDbFile);
-        if (!newDbFile.equals(currentDbFile)) {
-            FileLog.d(TAG, "Restored grid is disabled : " + currentGridName
-                    + ", migrating to: " + newGridName
-                    + ", removing all other grid db files");
-            for (String gridDbFile : LauncherFiles.GRID_DB_FILES) {
-                if (gridDbFile.equals(currentDbFile)) {
-                    continue;
-                }
-                if (context.getDatabasePath(gridDbFile).delete()) {
-                    FileLog.d(TAG, "Removed old grid db file: " + gridDbFile);
-                }
-            }
-            setCurrentGrid(context, newGridName);
+        if (mLandscapeModePreferenceListener != null) {
+            LauncherPrefs.INSTANCE.executeIfCreated(
+                    lp -> lp.removeListener(mLandscapeModePreferenceListener, FIXED_LANDSCAPE_MODE)
+            );
         }
     }
 
@@ -336,25 +345,48 @@
     }
 
     private String initGrid(Context context, String gridName) {
-        Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
-        @DeviceType int deviceType = displayInfo.getDeviceType();
+        if (!Flags.oneGridSpecs() && (isFixedLandscapeMode || FIXED_LANDSCAPE_MODE.get(context))) {
+            LauncherPrefs.get(context).put(FIXED_LANDSCAPE_MODE, false);
+            isFixedLandscapeMode = false;
+        }
 
-        ArrayList<DisplayOption> allOptions =
-                getPredefinedDeviceProfiles(context, gridName, deviceType,
-                        RestoreDbTask.isPending(context));
+        Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
+
+        List<DisplayOption> allOptions = getPredefinedDeviceProfiles(
+                context,
+                gridName,
+                displayInfo,
+                RestoreDbTask.isPending(context),
+                FIXED_LANDSCAPE_MODE.get(context)
+        );
+
+        // Filter out options that don't have the same number of columns as the grid
+        DeviceGridState deviceGridState = new DeviceGridState(context);
+        List<DisplayOption> allOptionsFilteredByColCount =
+                filterByColumnCount(allOptions, deviceGridState.getColumns());
+
         DisplayOption displayOption =
-                invDistWeightedInterpolate(displayInfo, allOptions, deviceType);
-        initGrid(context, displayInfo, displayOption, deviceType);
+                invDistWeightedInterpolate(displayInfo, allOptionsFilteredByColCount.isEmpty()
+                                ? new ArrayList<>(allOptions)
+                                : new ArrayList<>(allOptionsFilteredByColCount),
+                        displayInfo.getDeviceType());
+        initGrid(context, displayInfo, displayOption);
         return displayOption.grid.name;
     }
 
+    private List<DisplayOption> filterByColumnCount(
+            List<DisplayOption> allOptions, int numColumns) {
+        return allOptions.stream().filter(
+                option -> option.grid.numColumns == numColumns).toList();
+    }
+
     /**
      * @deprecated This is a temporary solution because on the backup and restore case we modify the
      * IDP, this resets it. b/332974074
      */
     @Deprecated
     public void reset(Context context) {
-        initGrid(context, getCurrentGridName(context));
+        initGrid(context, getDefaultGridName(context));
     }
 
     @VisibleForTesting
@@ -362,8 +394,7 @@
         return new InvariantDeviceProfile().initGrid(context, null);
     }
 
-    private void initGrid(Context context, Info displayInfo, DisplayOption displayOption,
-            @DeviceType int deviceType) {
+    private void initGrid(Context context, Info displayInfo, DisplayOption displayOption) {
         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
         GridOption closestProfile = displayOption.grid;
         numRows = closestProfile.numRows;
@@ -382,6 +413,7 @@
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
         workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
+        rowCountSpecsId = closestProfile.mRowCountSpecsId;
         workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
         allAppsSpecsId = closestProfile.mAllAppsSpecsId;
         allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
@@ -395,7 +427,8 @@
         allAppsCellSpecsTwoPanelId = closestProfile.mAllAppsCellSpecsTwoPanelId;
         numAllAppsRowsForCellHeightCalculation =
                 closestProfile.mNumAllAppsRowsForCellHeightCalculation;
-        this.deviceType = deviceType;
+        this.deviceType = displayInfo.getDeviceType();
+        this.displayInfo = displayInfo;
 
         inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
 
@@ -439,6 +472,9 @@
 
         startAlignTaskbar = displayOption.startAlignTaskbar;
 
+        // Fixed Landscape mode
+        isFixedLandscapeMode = FIXED_LANDSCAPE_MODE.get(context) && Flags.oneGridSpecs();
+
         // If the partner customization apk contains any grid overrides, apply them
         // Supported overrides: numRows, numColumns, iconSize
         applyPartnerDeviceProfileOverrides(context, metrics);
@@ -494,10 +530,35 @@
         mChangeListeners.remove(listener);
     }
 
+    /**
+     * Updates the current grid, this triggers a new IDP, reloads the database and triggers a grid
+     * migration.
+     */
+    public void setCurrentGrid(Context context, String newGridName) {
+        LauncherPrefs.get(context).put(GRID_NAME, newGridName);
+        MAIN_EXECUTOR.execute(() -> {
+            Trace.beginSection("InvariantDeviceProfile#setCurrentGrid");
+            onConfigChanged(context.getApplicationContext());
+            Trace.endSection();
+        });
+    }
 
-    public void setCurrentGrid(Context context, String gridName) {
-        LauncherPrefs.get(context).put(GRID_NAME, gridName);
-        MAIN_EXECUTOR.execute(() -> onConfigChanged(context.getApplicationContext()));
+    /**
+     * Updates the fixed landscape mode, this triggers a new IDP, reloads the database and triggers
+     * a grid migration.
+     */
+    public void setFixedLandscape(Context context, boolean isFixedLandscape) {
+        this.isFixedLandscapeMode = isFixedLandscape;
+        if (isFixedLandscape) {
+            // When in isFixedLandscape there should only be one default grid to choose from
+            MAIN_EXECUTOR.execute(() -> {
+                Trace.beginSection("InvariantDeviceProfile#setFixedLandscape");
+                onConfigChanged(context.getApplicationContext());
+                Trace.endSection();
+            });
+        } else {
+            setCurrentGrid(context, LauncherPrefs.get(context).get(GRID_NAME));
+        }
     }
 
     private Object[] toModelState() {
@@ -521,8 +582,19 @@
         }
     }
 
-    private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
-            String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
+    private static boolean firstGridFilter(GridOption gridOption, int deviceType,
+            boolean allowDisabledGrid, boolean isFixedLandscapeMode) {
+        return (gridOption.isEnabled(deviceType) || allowDisabledGrid)
+                && gridOption.filterByFlag(deviceType, isFixedLandscapeMode);
+    }
+
+    private static List<DisplayOption> getPredefinedDeviceProfiles(
+            Context context,
+            String gridName,
+            Info displayInfo,
+            boolean allowDisabledGrid,
+            boolean isFixedLandscapeMode
+    ) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
 
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
@@ -532,9 +604,10 @@
                     parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                 if ((type == XmlPullParser.START_TAG)
                         && GridOption.TAG_NAME.equals(parser.getName())) {
-
-                    GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser));
-                    if (gridOption.isEnabled(deviceType) || allowDisabledGrid) {
+                    GridOption gridOption = new GridOption(
+                            context, Xml.asAttributeSet(parser), displayInfo);
+                    if (firstGridFilter(gridOption, displayInfo.getDeviceType(), allowDisabledGrid,
+                            isFixedLandscapeMode)) {
                         final int displayDepth = parser.getDepth();
                         while (((type = parser.next()) != XmlPullParser.END_TAG
                                 || parser.getDepth() > displayDepth)
@@ -551,23 +624,25 @@
         } catch (IOException | XmlPullParserException e) {
             throw new RuntimeException(e);
         }
-
         ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
         if (!TextUtils.isEmpty(gridName)) {
             for (DisplayOption option : profiles) {
-                if (gridName.equals(option.grid.name)
-                        && (option.grid.isEnabled(deviceType) || allowDisabledGrid)) {
+                if (gridName.equals(option.grid.name) && (option.grid.isEnabled(
+                        displayInfo.getDeviceType()) || allowDisabledGrid)) {
                     filteredProfiles.add(option);
                 }
             }
         }
-        if (filteredProfiles.isEmpty()) {
-            // No grid found, use the default options
+        if (filteredProfiles.isEmpty() && TextUtils.isEmpty(gridName)) {
+            // Use the default options since gridName is empty and there's no valid grids.
             for (DisplayOption option : profiles) {
                 if (option.canBeDefault) {
                     filteredProfiles.add(option);
                 }
             }
+        } else if (filteredProfiles.isEmpty()) {
+            // In this case we had a grid selected but we couldn't find it.
+            filteredProfiles.addAll(profiles);
         }
         if (filteredProfiles.isEmpty()) {
             throw new RuntimeException("No display option with canBeDefault=true");
@@ -576,6 +651,70 @@
     }
 
     /**
+     * Parses through the xml to find NumRows specs. Then calls findBestRowCount to get the correct
+     * row count for this GridOption.
+     *
+     * @return the result of {@link #findBestRowCount(List, Info)}.
+     */
+    public static NumRows getRowCount(ResourceHelper resourceHelper, Context context,
+            Info displayInfo) {
+        ArrayList<NumRows> rowCounts = new ArrayList<>();
+
+        try (XmlResourceParser parser = resourceHelper.getXml()) {
+            final int depth = parser.getDepth();
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG
+                    || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if ((type == XmlPullParser.START_TAG)
+                        && "NumRows".equals(parser.getName())) {
+                    rowCounts.add(new NumRows(context, Xml.asAttributeSet(parser)));
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+
+        return findBestRowCount(rowCounts, displayInfo);
+    }
+
+    /**
+     * @return the biggest row count that fits the display dimensions spec using NumRows to
+     * determine that. If no best row count is found, return -1.
+     */
+    public static NumRows findBestRowCount(List<NumRows> list, Info displayInfo) {
+        int minWidthPx = Integer.MAX_VALUE;
+        int minHeightPx = Integer.MAX_VALUE;
+        for (WindowBounds bounds : displayInfo.supportedBounds) {
+            boolean isTablet = displayInfo.isTablet(bounds);
+            if (isTablet && displayInfo.getDeviceType() == TYPE_MULTI_DISPLAY) {
+                // For split displays, take half width per page
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+            } else if (!isTablet && bounds.isLandscape()) {
+                // We will use transposed layout in this case
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
+            } else {
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+            }
+        }
+
+        NumRows selectedRow = null;
+        for (NumRows item: list) {
+            if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
+                if (selectedRow == null || selectedRow.mNumRowsNew < item.mNumRowsNew) {
+                    selectedRow = item;
+                }
+            }
+        }
+        if (selectedRow != null) {
+            return selectedRow;
+        }
+        return null;
+    }
+
+    /**
      * Returns the GridOption associated to the given file name or null if the fileName is not
      * supported.
      * Ej, launcher.db -> "normal grid", launcher_4_by_4.db -> "practical grid"
@@ -592,7 +731,6 @@
      * supported. Ej. 4x4 -> normal, 5x4 -> practical, etc.
      * (Note: the name of the grid can be different for the same grid size depending of
      * the values of the InvariantDeviceProfile)
-     *
      */
     public String getGridNameFromSize(Context context, Point size) {
         return parseAllGridOptions(context).stream()
@@ -618,18 +756,18 @@
      * @return all the grid options that can be shown on the device
      */
     public List<GridOption> parseAllGridOptions(Context context) {
-        return parseAllDefinedGridOptions(context)
+        return parseAllDefinedGridOptions(context, displayInfo)
                 .stream()
                 .filter(go -> go.isEnabled(deviceType))
+                .filter(go -> go.filterByFlag(deviceType, isFixedLandscapeMode))
                 .collect(Collectors.toList());
     }
 
     /**
      * @return all the grid options that can be shown on the device
      */
-    public static List<GridOption> parseAllDefinedGridOptions(Context context) {
+    public static List<GridOption> parseAllDefinedGridOptions(Context context, Info displayInfo) {
         List<GridOption> result = new ArrayList<>();
-
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
             int type;
@@ -637,7 +775,7 @@
                     || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                 if ((type == XmlPullParser.START_TAG)
                         && GridOption.TAG_NAME.equals(parser.getName())) {
-                    result.add(new GridOption(context, Xml.asAttributeSet(parser)));
+                    result.add(new GridOption(context, Xml.asAttributeSet(parser), displayInfo));
                 }
             }
         } catch (IOException | XmlPullParserException e) {
@@ -704,7 +842,7 @@
     }
 
     private static DisplayOption invDistWeightedInterpolate(
-            Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType) {
+            Info displayInfo, List<DisplayOption> points, @DeviceType int deviceType) {
         int minWidthPx = Integer.MAX_VALUE;
         int minHeightPx = Integer.MAX_VALUE;
         for (WindowBounds bounds : displayInfo.supportedBounds) {
@@ -728,7 +866,7 @@
         float height = dpiFromPx(minHeightPx, displayInfo.getDensityDpi());
 
         // Sort the profiles based on the closeness to the device size
-        Collections.sort(points, (a, b) ->
+        points.sort((a, b) ->
                 Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
                         dist(width, height, b.minWidthDps, b.minHeightDps)));
 
@@ -850,6 +988,7 @@
         private static final int DONT_INLINE_QSB = 0;
 
         public final String name;
+        public final String title;
         public final int numRows;
         public final int numColumns;
         public final int numSearchContainerColumns;
@@ -876,6 +1015,7 @@
         private final int demoModeLayoutId;
 
         private final boolean isScalable;
+        private final boolean mIsDualGrid;
         private final int devicePaddingId;
         private final int mWorkspaceSpecsId;
         private final int mWorkspaceSpecsTwoPanelId;
@@ -889,22 +1029,39 @@
         private final int mWorkspaceCellSpecsTwoPanelId;
         private final int mAllAppsCellSpecsId;
         private final int mAllAppsCellSpecsTwoPanelId;
+        private final int mRowCountSpecsId;
+        private final boolean mIsFixedLandscape;
 
-        public GridOption(Context context, AttributeSet attrs) {
+        public GridOption(Context context, AttributeSet attrs, Info displayInfo) {
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.GridDisplayOption);
             name = a.getString(R.styleable.GridDisplayOption_name);
-            numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+            title = a.getString(R.styleable.GridDisplayOption_title);
+            deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
+                    DEVICE_CATEGORY_ALL);
+            mRowCountSpecsId = a.getResourceId(
+                    R.styleable.GridDisplayOption_rowCountSpecsId, INVALID_RESOURCE_HANDLE);
+            mIsDualGrid = a.getBoolean(R.styleable.GridDisplayOption_isDualGrid, false);
+            if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
+                ResourceHelper resourceHelper = new ResourceHelper(context, mRowCountSpecsId);
+                NumRows numR = getRowCount(resourceHelper, context, displayInfo);
+                numRows = numR.mNumRowsNew;
+                dbFile = numR.mDbFile;
+                defaultLayoutId = numR.mDefaultLayoutId;
+                demoModeLayoutId = numR.mDemoModeLayoutId;
+            } else {
+                numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+                dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
+                defaultLayoutId = a.getResourceId(
+                        R.styleable.GridDisplayOption_defaultLayoutId, 0);
+                demoModeLayoutId = a.getResourceId(
+                        R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId);
+            }
+
             numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
             numSearchContainerColumns = a.getInt(
                     R.styleable.GridDisplayOption_numSearchContainerColumns, numColumns);
 
-            dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
-            defaultLayoutId = a.getResourceId(
-                    R.styleable.GridDisplayOption_defaultLayoutId, 0);
-            demoModeLayoutId = a.getResourceId(
-                    R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId);
-
             allAppsStyle = a.getResourceId(R.styleable.GridDisplayOption_allAppsStyle,
                     R.style.AllAppsStyleDefault);
             numAllAppsColumns = a.getInt(
@@ -964,8 +1121,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 +1174,8 @@
                 mNumAllAppsRowsForCellHeightCalculation = numRows;
             }
 
+            mIsFixedLandscape = a.getBoolean(R.styleable.GridDisplayOption_isFixedLandscape, false);
+
             int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
                     DONT_INLINE_QSB);
             inlineQsb[INDEX_DEFAULT] =
@@ -1048,6 +1205,47 @@
                     return false;
             }
         }
+
+        public boolean isNewGridOption() {
+            return mRowCountSpecsId != INVALID_RESOURCE_HANDLE;
+        }
+
+        public boolean filterByFlag(int deviceType, boolean isFixedLandscape) {
+            if (deviceType == TYPE_TABLET) {
+                return Flags.oneGridRotationHandling() == mIsDualGrid;
+            }
+
+            if (isFixedLandscape) {
+                return Flags.oneGridSpecs() && mIsFixedLandscape;
+            }
+
+            return ((Flags.oneGridSpecs() == isNewGridOption()) && !mIsFixedLandscape);
+        }
+    }
+
+    public static final class NumRows {
+        final int mNumRowsNew;
+        final float mMinDeviceWidthPx;
+        final float mMinDeviceHeightPx;
+        final String mDbFile;
+        final int mDefaultLayoutId;
+        final int mDemoModeLayoutId;
+
+
+        NumRows(Context context, AttributeSet attrs) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumRows);
+
+            mNumRowsNew = (int) a.getFloat(R.styleable.NumRows_numRowsNew, 0);
+            mMinDeviceWidthPx = a.getFloat(R.styleable.NumRows_minDeviceWidthPx, 0);
+            mMinDeviceHeightPx = a.getFloat(R.styleable.NumRows_minDeviceHeightPx, 0);
+            mDbFile = a.getString(R.styleable.NumRows_dbFile);
+            mDefaultLayoutId = a.getResourceId(
+                    R.styleable.NumRows_defaultLayoutId, 0);
+            mDemoModeLayoutId = a.getResourceId(
+                    R.styleable.NumRows_demoModeLayoutId, mDefaultLayoutId);
+
+            a.recycle();
+        }
     }
 
     @VisibleForTesting
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index cb897dc..305941e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -70,12 +70,12 @@
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW;
 import static com.android.launcher3.logging.StatsLogManager.EventEnum;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
@@ -134,14 +134,12 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.text.method.TextKeyListener;
-import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MotionEvent;
 import android.view.View;
@@ -188,7 +186,6 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.LauncherDragController;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
@@ -214,7 +211,6 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupDataProvider;
@@ -228,10 +224,10 @@
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.ActivityResultInfo;
-import com.android.launcher3.util.ActivityTracker;
 import com.android.launcher3.util.BackPressHandler;
 import com.android.launcher3.util.CannedAnimationCoordinator;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ContextTracker;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInflater;
@@ -244,6 +240,7 @@
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener;
 import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.StableViewInfo;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
@@ -266,6 +263,7 @@
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 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.launcher3.widget.util.WidgetSizes;
 import com.android.systemui.plugins.LauncherOverlayPlugin;
 import com.android.systemui.plugins.PluginListener;
@@ -281,6 +279,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -294,7 +293,8 @@
         PluginListener<LauncherOverlayPlugin> {
     public static final String TAG = "Launcher";
 
-    public static final ActivityTracker<Launcher> ACTIVITY_TRACKER = new ActivityTracker<>();
+    public static final ContextTracker.ActivityTracker<Launcher> ACTIVITY_TRACKER =
+            new ContextTracker.ActivityTracker<>();
 
     static final boolean LOGD = false;
 
@@ -371,6 +371,7 @@
     private LauncherAccessibilityDelegate mAccessibilityDelegate;
 
     private PopupDataProvider mPopupDataProvider;
+    private WidgetPickerDataProvider mWidgetPickerDataProvider;
 
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
@@ -413,6 +414,7 @@
 
     private final List<BackPressHandler> mBackPressedHandlers = new ArrayList<>();
     private boolean mIsColdStartupAfterReboot;
+    private boolean mForceConfigUpdate;
 
     private boolean mIsNaturalScrollingEnabled;
 
@@ -436,6 +438,10 @@
         mIsColdStartupAfterReboot = sIsNewProcess
             && !LockedUserState.get(this).isUserUnlockedAtLauncherStartup();
         if (mIsColdStartupAfterReboot) {
+            /*
+             * This trace is used to calculate the time from create to the point that icons are
+             * visible.
+             */
             Trace.beginAsyncSection(
                     COLD_STARTUP_TRACE_METHOD_NAME, COLD_STARTUP_TRACE_COOKIE);
         }
@@ -445,12 +451,10 @@
                 .logStart(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION)
                 .logStart(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
         // Only use a hard-coded cookie since we only want to trace this once.
-        if (Utilities.ATLEAST_S) {
-            Trace.beginAsyncSection(
-                    DISPLAY_WORKSPACE_TRACE_METHOD_NAME, DISPLAY_WORKSPACE_TRACE_COOKIE);
-            Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
-                    DISPLAY_ALL_APPS_TRACE_COOKIE);
-        }
+        Trace.beginAsyncSection(
+                DISPLAY_WORKSPACE_TRACE_METHOD_NAME, DISPLAY_WORKSPACE_TRACE_COOKIE);
+        Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
+                DISPLAY_ALL_APPS_TRACE_COOKIE);
         TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT);
         if (DEBUG_STRICT_MODE) {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
@@ -532,6 +536,8 @@
                 mFocusHandler, new CellLayout(mWorkspace.getContext(), mWorkspace));
 
         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
+        mWidgetPickerDataProvider = new WidgetPickerDataProvider();
+        PillColorProvider.getInstance(mWorkspace.getContext()).registerObserver();
 
         boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
         if (internalStateHandled) {
@@ -728,13 +734,6 @@
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
         mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
-        // Starting with Android S, onEnterAnimationComplete is sent immediately
-        // causing the surface to get removed before the animation completed (b/175345344).
-        // Instead we rely on next user touch event to remove the view and optionally a callback
-        // from system from Android T onwards.
-        if (!Utilities.ATLEAST_S) {
-            AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
-        }
     }
 
     @Override
@@ -761,10 +760,9 @@
     protected void onHandleConfigurationChanged() {
         Trace.beginSection("Launcher#onHandleconfigurationChanged");
         try {
-            if (!initDeviceProfile(mDeviceProfile.inv)) {
+            if (!initDeviceProfile(mDeviceProfile.inv) && !mForceConfigUpdate) {
                 return;
             }
-
             dispatchDeviceProfileChanged();
             reapplyUi();
             mDragLayer.recreateControllers();
@@ -775,10 +773,20 @@
             mModel.rebindCallbacks();
             updateDisallowBack();
         } finally {
+            mForceConfigUpdate = false;
             Trace.endSection();
         }
     }
 
+    private void updateFixedLandscape() {
+        if (!com.android.launcher3.Flags.oneGridSpecs()) {
+            return;
+        }
+        getRotationHelper().setFixedLandscape(
+                Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscapeMode
+        );
+    }
+
     public void onAssistantVisibilityChanged(float visibility) {
         mHotseat.getQsb().setAlpha(1f - visibility);
     }
@@ -807,6 +815,7 @@
                     mDeviceProfile.numShownHotseatIcons);
         }
         mModelWriter = mModel.getWriter(true, mCellPosMapper, this);
+        updateFixedLandscape();
         return true;
     }
 
@@ -816,7 +825,7 @@
             View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container);
             if (collectionIcon instanceof FolderIcon folderIcon
                     && collectionIcon.getTag() instanceof FolderInfo) {
-                if (new FolderGridOrganizer(getDeviceProfile())
+                if (createFolderGridOrganizer(getDeviceProfile())
                         .setFolderInfo((FolderInfo) folderIcon.getTag())
                         .isItemInPreview(info.rank)) {
                     folderIcon.invalidate();
@@ -1246,9 +1255,7 @@
      * Returns {@link EventEnum} that should be logged when Launcher enters into AllApps state.
      */
     protected Optional<EventEnum> getAllAppsEntryEvent() {
-        return Optional.of(FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                ? LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH
-                : LAUNCHER_ALLAPPS_ENTRY);
+        return Optional.of(LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH);
     }
 
     @Override
@@ -1349,7 +1356,8 @@
         if (requestArgs != null) {
             setWaitingForResult(requestArgs);
         }
-        mPendingActivityRequestCode = savedState.getInt(RUNTIME_STATE_PENDING_REQUEST_CODE);
+        mPendingActivityRequestCode = savedState.getInt(
+                RUNTIME_STATE_PENDING_REQUEST_CODE, mPendingActivityRequestCode);
 
         mPendingActivityResult = savedState.getParcelable(RUNTIME_STATE_PENDING_ACTIVITY_RESULT);
 
@@ -1407,15 +1415,6 @@
                 this, R.attr.isWorkspaceDarkText) ? Color.BLACK : Color.WHITE);
     }
 
-    @Override
-    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
-        if (WorkspacePageIndicator.class.getName().equals(name)) {
-            return LayoutInflater.from(context).inflate(R.layout.page_indicator_dots,
-                    (ViewGroup) parent, false);
-        }
-        return super.onCreateView(parent, name, context, attrs);
-    }
-
     /**
      * Add a shortcut to the workspace or to a Folder.
      *
@@ -1792,7 +1791,7 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        ACTIVITY_TRACKER.onActivityDestroyed(this);
+        ACTIVITY_TRACKER.onContextDestroyed(this);
 
         SettingsCache.INSTANCE.get(this).unregister(TOUCHPAD_NATURAL_SCROLLING,
                 mNaturalScrollingChangedListener);
@@ -1815,6 +1814,7 @@
         // changes while launcher is still loading.
         getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener);
         mOverlayManager.onActivityDestroyed();
+        PillColorProvider.getInstance(mWorkspace.getContext()).unregisterObserver();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1888,13 +1888,18 @@
             }
         }
 
-        // Exit spring loaded mode if necessary after adding the widget
-        Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
-                : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+        // Exit spring loaded mode if necessary after adding the widget; unless config activity was
+        // started.
+        Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null : () -> mStateManager.goToState(
+                NORMAL, SPRING_LOADED_EXIT_DELAY);
         completeAddAppWidget(appWidgetId, info, boundWidget,
                 addFlowHandler.getProviderInfo(this), addFlowHandler.needsConfigure(),
                 false, widgetPreviewBitmap);
-        mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
+        // Remove extra screen if widget drop concluded. If a config activity was started, extra
+        // screen will be removed when we get back its result.
+        if (!isActivityStarted) {
+            mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
+        }
     }
 
     public void addPendingItem(PendingAddItemInfo info, int container, int screenId,
@@ -2393,10 +2398,6 @@
                     .logEnd(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION)
                     .log()
                     .reset();
-            if (mIsColdStartupAfterReboot) {
-                Trace.endAsyncSection(COLD_STARTUP_TRACE_METHOD_NAME,
-                        COLD_STARTUP_TRACE_COOKIE);
-            }
         });
     }
 
@@ -2405,6 +2406,10 @@
             RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) {
         mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, onCompleteSignal,
                 workspaceItemCount, isBindSync);
+        if (mIsColdStartupAfterReboot) {
+            Trace.endAsyncSection(COLD_STARTUP_TRACE_METHOD_NAME,
+                    COLD_STARTUP_TRACE_COOKIE);
+        }
     }
 
     /**
@@ -2429,17 +2434,16 @@
      * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close
      * animation.
      *
-     * @param preferredItemId The id of the preferred item to match to if it exists,
-     *                        or ItemInfo#NO_MATCHING_ID if you want to not match by item id
+     * @param svi The StableViewInfo of the preferred item to match to if it exists or null
      * @param packageName The package name of the app to match.
      * @param user The user of the app to match.
      * @param supportsAllAppsState If true and we are in All Apps state, looks for view in All Apps.
      *                             Else we only looks on the workspace.
      */
-    public @Nullable View getFirstMatchForAppClose(int preferredItemId, String packageName,
+    public @Nullable View getFirstMatchForAppClose(
+            @Nullable StableViewInfo svi, String packageName,
             UserHandle user, boolean supportsAllAppsState) {
-        final Predicate<ItemInfo> preferredItem = info ->
-                info != null && info.id == preferredItemId;
+        final Predicate<ItemInfo> preferredItem = svi == null ? i -> false : svi::matches;
         final Predicate<ItemInfo> packageAndUserAndApp = info ->
                 info != null
                         && info.itemType == ITEM_TYPE_APPLICATION
@@ -2530,6 +2534,9 @@
         final int itemCount = container.getChildCount();
         for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
             View item = container.getChildAt(itemIdx);
+            if (item.getVisibility() != View.VISIBLE) {
+                continue;
+            }
             if (item instanceof ViewGroup viewGroup) {
                 View view = mapOverViewGroup(viewGroup, op);
                 if (view != null) {
@@ -2584,10 +2591,8 @@
     public void bindAllApplications(AppInfo[] apps, int flags,
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         mModelCallbacks.bindAllApplications(apps, flags, packageUserKeytoUidMap);
-        if (Utilities.ATLEAST_S) {
-            Trace.endAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
-                    DISPLAY_ALL_APPS_TRACE_COOKIE);
-        }
+        Trace.endAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
+                DISPLAY_ALL_APPS_TRACE_COOKIE);
     }
 
     /**
@@ -2636,8 +2641,9 @@
      * See {@code LauncherBindingDelegate}
      */
     @Override
-    public void bindAllWidgets(final List<WidgetsListBaseEntry> allWidgets) {
-        mModelCallbacks.bindAllWidgets(allWidgets);
+    public void bindAllWidgets(@NonNull final List<WidgetsListBaseEntry> allWidgets,
+            @NonNull final List<WidgetsListBaseEntry> defaultWidgets) {
+        mModelCallbacks.bindAllWidgets(allWidgets, defaultWidgets);
     }
 
     @Override
@@ -2680,6 +2686,7 @@
             }
 
             writer.println(prefix + "  Hotseat");
+            mHotseat.dump(prefix, writer);
             ViewGroup layout = mHotseat.getShortcutsAndWidgets();
             for (int j = 0; j < layout.getChildCount(); j++) {
                 Object tag = layout.getChildAt(j).getTag();
@@ -2698,12 +2705,31 @@
         writer.println(prefix + "\tmAppWidgetHolder.isListening: "
                 + mAppWidgetHolder.isListening());
 
+        // b/349929393
+        // The only way to reproduce this bug is to ensure that onLayout never gets called. This
+        // theoretically is impossible, so these logs are being added to test if that actually is
+        // what is happening.
+        writer.println(prefix + "\tmWorkspace.mHasOnLayoutBeenCalled="
+                + mWorkspace.mHasOnLayoutBeenCalled);
+        for (int i = 0; i < mWorkspace.getPageCount(); i++) {
+            CellLayout cellLayout = (CellLayout) mWorkspace.getPageAt(i);
+            writer.println(prefix + "\tcellLayout." + i + ".mHasOnLayoutBeenCalled="
+                    + cellLayout.mHasOnLayoutBeenCalled);
+            writer.println(prefix + "\tshortcutAndWidgetContainer." + i + ".mHasOnLayoutBeenCalled="
+                    + cellLayout.getShortcutsAndWidgets().mHasOnLayoutBeenCalled);
+        }
+
         // Extra logging for general debugging
         mDragLayer.dump(prefix, writer);
         mStateManager.dump(prefix, writer);
         mPopupDataProvider.dump(prefix, writer);
+        mWidgetPickerDataProvider.dump(prefix, writer);
         mDeviceProfile.dump(this, prefix, writer);
         mAppsView.getAppsStore().dump(prefix, writer);
+        mAppsView.getPersonalAppList().dump(prefix, writer);
+        if (mAppsView.shouldShowTabs()) {
+            mAppsView.getWorkAppList().dump(prefix, writer);
+        }
 
         try {
             FileLog.flushAll(writer);
@@ -3011,11 +3037,18 @@
         return mPopupDataProvider;
     }
 
+    @NonNull
+    @Override
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
+    }
+
     @Override
     public DotInfo getDotInfoForItem(ItemInfo info) {
         return mPopupDataProvider.getDotInfoForItem(info);
     }
 
+    @NonNull
     public LauncherOverlayManager getOverlayManager() {
         return mOverlayManager;
     }
@@ -3135,6 +3168,13 @@
         return mAnimationCoordinator;
     }
 
+    /**
+     * Set to force config update when set to true next time onHandleConfigurationChanged is called.
+     */
+    public void setForceConfigUpdate(boolean forceConfigUpdate) {
+        mForceConfigUpdate = forceConfigUpdate;
+    }
+
     @Override
     public View.OnLongClickListener getAllAppsItemLongClickListener() {
         return ItemLongClickListener.INSTANCE_ALL_APPS;
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 85c8b57..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;
@@ -115,6 +116,7 @@
         if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
             ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
             params.setEnableUnarchivalConfirmation(false);
+            params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps());
             launcherApps.setArchiveCompatibility(params);
         }
 
@@ -162,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(
@@ -175,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);
         });
 
@@ -197,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);
@@ -265,7 +267,7 @@
     }
 
     private class IconObserver
-            implements IconProvider.IconChangeListener, OnSharedPreferenceChangeListener {
+            implements IconProvider.IconChangeListener, LauncherPrefChangeListener {
 
         @Override
         public void onAppIconChanged(String packageName, UserHandle user) {
@@ -287,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 40873be..678901b 100644
--- a/src/com/android/launcher3/LauncherApplication.java
+++ b/src/com/android/launcher3/LauncherApplication.java
@@ -17,14 +17,45 @@
 
 import android.app.Application;
 
+import com.android.launcher3.dagger.DaggerLauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
 /**
  * Main application class for Launcher
  */
 public class LauncherApplication extends Application {
 
+    private volatile LauncherBaseAppComponent mAppComponent;
     @Override
     public void onCreate() {
         super.onCreate();
         MainProcessInitializer.initialize(this);
     }
+
+    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()
+        // method is defined in the super class LauncherBaseComponent#Builder.
+        return (LauncherAppComponent) mAppComponent;
+    }
+
+    /**
+     * Init with the desired dagger component.
+     */
+    public void initDaggerComponent(LauncherAppComponent.Builder componentBuilder) {
+        mAppComponent = componentBuilder.appContext(this).build();
+    }
 }
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
index 2617b93..a96495d 100644
--- a/src/com/android/launcher3/LauncherBackupAgent.java
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -1,5 +1,7 @@
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED;
+
 import android.app.backup.BackupAgent;
 import android.app.backup.BackupDataInput;
 import android.app.backup.BackupDataOutput;
@@ -10,10 +12,13 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 
 public class LauncherBackupAgent extends BackupAgent {
-
     private static final String TAG = "LauncherBackupAgent";
+    private static final String DB_FILE_PREFIX = "launcher";
+    private static final String DB_FILE_SUFFIX = ".db";
 
     @Override
     public void onCreate() {
@@ -47,7 +52,34 @@
 
     @Override
     public void onRestoreFinished() {
-        FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
         RestoreDbTask.setPending(this);
+        FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
+        markIfFilesWereNotActuallyRestored();
+    }
+
+    /**
+     * When restore is finished, we check to see if any db files were successfully restored. If not,
+     * our restore will fail later, but will report a different cause. This is important to split
+     * out the metric failures that are launcher's fault, and those that are due to bugs in the
+     * backup/restore code itself.
+     */
+    private void markIfFilesWereNotActuallyRestored() {
+        File directory = new File(getDatabasePath(InvariantDeviceProfile.INSTANCE.get(this).dbFile)
+                .getParent());
+        if (!directory.exists()) {
+            FileLog.e(TAG, "restore failed as target database directory doesn't exist");
+        } else {
+            // Check for any db file that was restored, and collect as list
+            String fileNames = Arrays.stream(directory.listFiles())
+                    .map(File::getName)
+                    .filter(n -> n.startsWith(DB_FILE_PREFIX) && n.endsWith(DB_FILE_SUFFIX))
+                    .collect(Collectors.joining(", "));
+            if (fileNames.isBlank()) {
+                FileLog.e(TAG, "no database files were successfully restored");
+                LauncherPrefs.get(this).putSync(NO_DB_FILES_RESTORED.to(true));
+            } else {
+                FileLog.d(TAG, "database files successfully restored: " + fileNames);
+            }
+        }
     }
 }
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index d730cea..df75470 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -16,11 +16,15 @@
     private static final String XML = ".xml";
 
     public static final String LAUNCHER_DB = "launcher.db";
+    public static final String LAUNCHER_5_BY_8_DB = "launcher_5_by_8.db";
     public static final String LAUNCHER_6_BY_5_DB = "launcher_6_by_5.db";
     public static final String LAUNCHER_4_BY_5_DB = "launcher_4_by_5.db";
+    public static final String LAUNCHER_4_BY_6_DB = "launcher_4_by_6.db";
+    public static final String LAUNCHER_5_BY_6_DB = "launcher_5_by_6.db";
     public static final String LAUNCHER_4_BY_4_DB = "launcher_4_by_4.db";
     public static final String LAUNCHER_3_BY_3_DB = "launcher_3_by_3.db";
     public static final String LAUNCHER_2_BY_2_DB = "launcher_2_by_2.db";
+    public static final String LAUNCHER_7_BY_3_DB = "launcher_7_by_3.db";
     public static final String BACKUP_DB = "backup.db";
     public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs";
     public static final String MANAGED_USER_PREFERENCES_KEY =
@@ -33,11 +37,15 @@
 
     public static final List<String> GRID_DB_FILES = Collections.unmodifiableList(Arrays.asList(
             LAUNCHER_DB,
+            LAUNCHER_5_BY_8_DB,
             LAUNCHER_6_BY_5_DB,
             LAUNCHER_4_BY_5_DB,
+            LAUNCHER_4_BY_6_DB,
+            LAUNCHER_5_BY_6_DB,
             LAUNCHER_4_BY_4_DB,
             LAUNCHER_3_BY_3_DB,
-            LAUNCHER_2_BY_2_DB));
+            LAUNCHER_2_BY_2_DB,
+            LAUNCHER_7_BY_3_DB));
 
     public static final List<String> OTHER_FILES = Collections.unmodifiableList(Arrays.asList(
             BACKUP_DB,
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
deleted file mode 100644
index ca1b2a9..0000000
--- a/src/com/android/launcher3/LauncherModel.java
+++ /dev/null
@@ -1,691 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
-
-import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
-import static com.android.launcher3.LauncherPrefs.WORK_EDU_STEP;
-import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
-import static com.android.launcher3.icons.cache.BaseIconCache.EMPTY_CLASS_NAME;
-import static com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE;
-import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_AVAILABLE;
-import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_UNAVAILABLE;
-import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInstaller;
-import android.content.pm.ShortcutInfo;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.celllayout.CellPosMapper;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.AddWorkspaceItemsTask;
-import com.android.launcher3.model.AllAppsList;
-import com.android.launcher3.model.BaseLauncherBinder;
-import com.android.launcher3.model.BgDataModel;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.model.CacheDataUpdatedTask;
-import com.android.launcher3.model.ItemInstallQueue;
-import com.android.launcher3.model.LoaderTask;
-import com.android.launcher3.model.ModelDbController;
-import com.android.launcher3.model.ModelDelegate;
-import com.android.launcher3.model.ModelLauncherCallbacks;
-import com.android.launcher3.model.ModelTaskController;
-import com.android.launcher3.model.ModelWriter;
-import com.android.launcher3.model.PackageInstallStateChangedTask;
-import com.android.launcher3.model.PackageUpdatedTask;
-import com.android.launcher3.model.ReloadStringCacheTask;
-import com.android.launcher3.model.ShortcutsChangedTask;
-import com.android.launcher3.model.UserLockStateChangedTask;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.pm.InstallSessionTracker;
-import com.android.launcher3.pm.PackageInstallInfo;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Preconditions;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.concurrent.CancellationException;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/**
- * Maintains in-memory state of the Launcher. It is expected that there should be only one
- * LauncherModel object held in a static. Also provide APIs for updating the database state
- * for the Launcher.
- */
-public class LauncherModel implements InstallSessionTracker.Callback {
-    private static final boolean DEBUG_RECEIVER = false;
-
-    static final String TAG = "Launcher.Model";
-
-    @NonNull
-    private final LauncherAppState mApp;
-    @NonNull
-    private final PackageManagerHelper mPmHelper;
-    @NonNull
-    private final ModelDbController mModelDbController;
-    @NonNull
-    private final Object mLock = new Object();
-    @Nullable
-    private LoaderTask mLoaderTask;
-    private boolean mIsLoaderTaskRunning;
-
-    // only allow this once per reboot to reload work apps
-    private boolean mShouldReloadWorkProfile = true;
-
-    // Indicates whether the current model data is valid or not.
-    // We start off with everything not loaded. After that, we assume that
-    // our monitoring of the package manager provides all updates and we never
-    // need to do a requery. This is only ever touched from the loader thread.
-    private boolean mModelLoaded;
-    private boolean mModelDestroyed = false;
-    public boolean isModelLoaded() {
-        synchronized (mLock) {
-            return mModelLoaded && mLoaderTask == null && !mModelDestroyed;
-        }
-    }
-
-    @NonNull
-    private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
-
-    // < only access in worker thread >
-    @NonNull
-    private final AllAppsList mBgAllAppsList;
-
-    /**
-     * All the static data should be accessed on the background thread, A lock should be acquired
-     * on this object when accessing any data from this model.
-     */
-    @NonNull
-    private final BgDataModel mBgDataModel = new BgDataModel();
-
-    @NonNull
-    private final ModelDelegate mModelDelegate;
-
-    private int mLastLoadId = -1;
-
-    // Runnable to check if the shortcuts permission has changed.
-    @NonNull
-    private final Runnable mDataValidationCheck = new Runnable() {
-        @Override
-        public void run() {
-            if (mModelLoaded) {
-                mModelDelegate.validateData();
-            }
-        }
-    };
-
-    LauncherModel(@NonNull final Context context, @NonNull final LauncherAppState app,
-            @NonNull final IconCache iconCache, @NonNull final AppFilter appFilter,
-            @NonNull final PackageManagerHelper pmHelper, final boolean isPrimaryInstance) {
-        mApp = app;
-        mPmHelper = pmHelper;
-        mModelDbController = new ModelDbController(context);
-        mBgAllAppsList = new AllAppsList(iconCache, appFilter);
-        mModelDelegate = ModelDelegate.newInstance(context, app, mPmHelper, mBgAllAppsList,
-                mBgDataModel, isPrimaryInstance);
-    }
-
-    @NonNull
-    public ModelDelegate getModelDelegate() {
-        return mModelDelegate;
-    }
-
-    public ModelDbController getModelDbController() {
-        return mModelDbController;
-    }
-
-    public ModelLauncherCallbacks newModelCallbacks() {
-        return new ModelLauncherCallbacks(this::enqueueModelUpdateTask);
-    }
-
-    /**
-     * Adds the provided items to the workspace.
-     */
-    public void addAndBindAddedWorkspaceItems(
-            @NonNull final List<Pair<ItemInfo, Object>> itemList) {
-        for (Callbacks cb : getCallbacks()) {
-            cb.preAddApps();
-        }
-        enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
-    }
-
-    @NonNull
-    public ModelWriter getWriter(final boolean verifyChanges, CellPosMapper cellPosMapper,
-            @Nullable final Callbacks owner) {
-        return new ModelWriter(mApp.getContext(), this, mBgDataModel, verifyChanges, cellPosMapper,
-                owner);
-    }
-
-    /**
-     * Called when the icon for an app changes, outside of package event
-     */
-    @WorkerThread
-    public void onAppIconChanged(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        // Update the icon for the calendar package
-        Context context = mApp.getContext();
-        enqueueModelUpdateTask(new PackageUpdatedTask(OP_UPDATE, user, packageName));
-
-        List<ShortcutInfo> pinnedShortcuts = new ShortcutRequest(context, user)
-                .forPackage(packageName).query(ShortcutRequest.PINNED);
-        if (!pinnedShortcuts.isEmpty()) {
-            enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, pinnedShortcuts, user,
-                    false));
-        }
-    }
-
-    /**
-     * Called when the workspace items have drastically changed
-     */
-    public void onWorkspaceUiChanged() {
-        MODEL_EXECUTOR.execute(mModelDelegate::workspaceLoadComplete);
-    }
-
-    /**
-     * Called when the model is destroyed
-     */
-    public void destroy() {
-        mModelDestroyed = true;
-        MODEL_EXECUTOR.execute(mModelDelegate::destroy);
-    }
-
-    public void onBroadcastIntent(@NonNull final Intent intent) {
-        if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=" + intent);
-        final String action = intent.getAction();
-        if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
-            // If we have changed locale we need to clear out the labels in all apps/workspace.
-            forceReload();
-        } else if (ACTION_DEVICE_POLICY_RESOURCE_UPDATED.equals(action)) {
-            enqueueModelUpdateTask(new ReloadStringCacheTask(mModelDelegate));
-        } else if (IS_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
-            for (Callbacks cb : getCallbacks()) {
-                if (cb instanceof Launcher) {
-                    ((Launcher) cb).recreate();
-                }
-            }
-        }
-    }
-
-    /**
-     * Called then there use a user event
-     * @see UserCache#addUserEventListener
-     */
-    public void onUserEvent(UserHandle user, String action) {
-        if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
-                && mShouldReloadWorkProfile) {
-            mShouldReloadWorkProfile = false;
-            forceReload();
-        } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
-                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
-            mShouldReloadWorkProfile = false;
-            enqueueModelUpdateTask(new PackageUpdatedTask(
-                    PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
-        } else if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
-                || UserCache.ACTION_PROFILE_UNLOCKED.equals(action)) {
-            enqueueModelUpdateTask(new UserLockStateChangedTask(
-                    user, UserCache.ACTION_PROFILE_UNLOCKED.equals(action)));
-        } else if (UserCache.ACTION_PROFILE_ADDED.equals(action)
-                || UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
-            forceReload();
-        } else if (ACTION_PROFILE_AVAILABLE.equals(action)
-                || ACTION_PROFILE_UNAVAILABLE.equals(action)) {
-            /*
-             * This broadcast is only available when android.os.Flags.allowPrivateProfile() is set.
-             * For Work-profile this broadcast will be sent in addition to
-             * ACTION_MANAGED_PROFILE_AVAILABLE/UNAVAILABLE.
-             * So effectively, this if block only handles the non-work profile case.
-             */
-            enqueueModelUpdateTask(new PackageUpdatedTask(
-                    PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
-        }
-        if (Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) {
-            LauncherPrefs.get(mApp.getContext()).put(WORK_EDU_STEP, 0);
-        }
-    }
-
-    /**
-     * Reloads the workspace items from the DB and re-binds the workspace. This should generally
-     * not be called as DB updates are automatically followed by UI update
-     */
-    public void forceReload() {
-        synchronized (mLock) {
-            // Stop any existing loaders first, so they don't set mModelLoaded to true later
-            stopLoader();
-            mModelLoaded = false;
-        }
-
-        // Start the loader if launcher is already running, otherwise the loader will run,
-        // the next time launcher starts
-        if (hasCallbacks()) {
-            startLoader();
-        }
-    }
-
-    /**
-     * Rebinds all existing callbacks with already loaded model
-     */
-    public void rebindCallbacks() {
-        if (hasCallbacks()) {
-            startLoader();
-        }
-    }
-
-    /**
-     * Removes an existing callback
-     */
-    public void removeCallbacks(@NonNull final Callbacks callbacks) {
-        synchronized (mCallbacksList) {
-            Preconditions.assertUIThread();
-            if (mCallbacksList.remove(callbacks)) {
-                if (stopLoader()) {
-                    // Rebind existing callbacks
-                    startLoader();
-                }
-            }
-        }
-    }
-
-    /**
-     * Adds a callbacks to receive model updates
-     * @return true if workspace load was performed synchronously
-     */
-    public boolean addCallbacksAndLoad(@NonNull final Callbacks callbacks) {
-        synchronized (mLock) {
-            addCallbacks(callbacks);
-            return startLoader(new Callbacks[] { callbacks });
-
-        }
-    }
-
-    /**
-     * Adds a callbacks to receive model updates
-     */
-    public void addCallbacks(@NonNull final Callbacks callbacks) {
-        Preconditions.assertUIThread();
-        synchronized (mCallbacksList) {
-            mCallbacksList.add(callbacks);
-        }
-    }
-
-    /**
-     * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
-     * @return true if the page could be bound synchronously.
-     */
-    public boolean startLoader() {
-        return startLoader(new Callbacks[0]);
-    }
-
-    private boolean startLoader(@NonNull final Callbacks[] newCallbacks) {
-        // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
-        ItemInstallQueue.INSTANCE.get(mApp.getContext())
-                .pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING);
-        synchronized (mLock) {
-            // If there is already one running, tell it to stop.
-            boolean wasRunning = stopLoader();
-            boolean bindDirectly = mModelLoaded && !mIsLoaderTaskRunning;
-            boolean bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.length == 0;
-            final Callbacks[] callbacksList = bindAllCallbacks ? getCallbacks() : newCallbacks;
-
-            if (callbacksList.length > 0) {
-                // Clear any pending bind-runnables from the synchronized load process.
-                for (Callbacks cb : callbacksList) {
-                    MAIN_EXECUTOR.execute(cb::clearPendingBinds);
-                }
-
-                BaseLauncherBinder launcherBinder = new BaseLauncherBinder(
-                        mApp, mBgDataModel, mBgAllAppsList, callbacksList);
-                if (bindDirectly) {
-                    // Divide the set of loaded items into those that we are binding synchronously,
-                    // and everything else that is to be bound normally (asynchronously).
-                    launcherBinder.bindWorkspace(bindAllCallbacks, /* isBindSync= */ true);
-                    // For now, continue posting the binding of AllApps as there are other
-                    // issues that arise from that.
-                    launcherBinder.bindAllApps();
-                    launcherBinder.bindDeepShortcuts();
-                    launcherBinder.bindWidgets();
-                    if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                        mModelDelegate.bindAllModelExtras(callbacksList);
-                    }
-                    return true;
-                } else {
-                    stopLoader();
-                    mLoaderTask = new LoaderTask(
-                            mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, launcherBinder);
-
-                    // Always post the loader task, instead of running directly
-                    // (even on same thread) so that we exit any nested synchronized blocks
-                    MODEL_EXECUTOR.post(mLoaderTask);
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * If there is already a loader task running, tell it to stop.
-     * @return true if an existing loader was stopped.
-     */
-    private boolean stopLoader() {
-        synchronized (mLock) {
-            LoaderTask oldTask = mLoaderTask;
-            mLoaderTask = null;
-            if (oldTask != null) {
-                oldTask.stopLocked();
-                return true;
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Loads the model if not loaded
-     * @param callback called with the data model upon successful load or null on model thread.
-     */
-    public void loadAsync(@NonNull final Consumer<BgDataModel> callback) {
-        synchronized (mLock) {
-            if (!mModelLoaded && !mIsLoaderTaskRunning) {
-                startLoader();
-            }
-        }
-        MODEL_EXECUTOR.post(() -> callback.accept(isModelLoaded() ? mBgDataModel : null));
-    }
-
-    @Override
-    public void onInstallSessionCreated(@NonNull final PackageInstallInfo sessionInfo) {
-        if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
-            enqueueModelUpdateTask((taskController, dataModel, apps) -> {
-                apps.addPromiseApp(mApp.getContext(), sessionInfo);
-                taskController.bindApplicationsIfNeeded();
-            });
-        }
-    }
-
-    @Override
-    public void onSessionFailure(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) -> {
-            IconCache iconCache = mApp.getIconCache();
-            final IntSet removedIds = new IntSet();
-            HashSet<WorkspaceItemInfo> archivedWorkspaceItemsToCacheRefresh = new HashSet<>();
-            boolean isAppArchived = PackageManagerHelper.INSTANCE.get(mApp.getContext())
-                    .isAppArchivedForUser(packageName, user);
-            synchronized (dataModel) {
-                if (isAppArchived) {
-                    // Remove package icon cache entry for archived app in case of a session
-                    // failure.
-                    mApp.getIconCache().remove(
-                            new ComponentName(packageName, packageName + EMPTY_CLASS_NAME),
-                            user);
-                }
-
-                for (ItemInfo info : dataModel.itemsIdMap) {
-                    if (info instanceof WorkspaceItemInfo
-                            && ((WorkspaceItemInfo) info).hasPromiseIconUi()
-                            && user.equals(info.user)
-                            && info.getIntent() != null) {
-                        if (TextUtils.equals(packageName, info.getIntent().getPackage())) {
-                            removedIds.add(info.id);
-                        }
-                        if (((WorkspaceItemInfo) info).isArchived()) {
-                            WorkspaceItemInfo workspaceItem = (WorkspaceItemInfo) info;
-                            // Refresh icons on the workspace for archived apps.
-                            iconCache.getTitleAndIcon(workspaceItem,
-                                    workspaceItem.usingLowResIcon());
-                            archivedWorkspaceItemsToCacheRefresh.add(workspaceItem);
-                        }
-                    }
-                }
-
-                if (isAppArchived) {
-                    apps.updateIconsAndLabels(new HashSet<>(List.of(packageName)), user);
-                }
-            }
-
-            if (!removedIds.isEmpty() && !isAppArchived) {
-                taskController.deleteAndBindComponentsRemoved(
-                        ItemInfoMatcher.ofItemIds(removedIds),
-                        "removed because install session failed");
-            }
-            if (!archivedWorkspaceItemsToCacheRefresh.isEmpty()) {
-                taskController.bindUpdatedWorkspaceItems(
-                        archivedWorkspaceItemsToCacheRefresh.stream().toList());
-            }
-            if (isAppArchived) {
-                taskController.bindApplicationsIfNeeded();
-            }
-        });
-    }
-
-    @Override
-    public void onPackageStateChanged(@NonNull final PackageInstallInfo installInfo) {
-        enqueueModelUpdateTask(new PackageInstallStateChangedTask(installInfo));
-    }
-
-    /**
-     * Updates the icons and label of all pending icons for the provided package name.
-     */
-    @Override
-    public void onUpdateSessionDisplay(@NonNull final PackageUserKey key,
-            @NonNull final PackageInstaller.SessionInfo info) {
-        mApp.getIconCache().updateSessionCache(key, info);
-
-        HashSet<String> packages = new HashSet<>();
-        packages.add(key.mPackageName);
-        enqueueModelUpdateTask(new CacheDataUpdatedTask(
-                CacheDataUpdatedTask.OP_SESSION_UPDATE, key.mUser, packages));
-    }
-
-    public class LoaderTransaction implements AutoCloseable {
-
-        @NonNull
-        private final LoaderTask mTask;
-
-        private LoaderTransaction(@NonNull final LoaderTask task) throws CancellationException {
-            synchronized (mLock) {
-                if (mLoaderTask != task) {
-                    throw new CancellationException("Loader already stopped");
-                }
-                mLastLoadId++;
-                mTask = task;
-                mIsLoaderTaskRunning = true;
-                mModelLoaded = false;
-            }
-        }
-
-        public void commit() {
-            synchronized (mLock) {
-                // Everything loaded bind the data.
-                mModelLoaded = true;
-            }
-        }
-
-        @Override
-        public void close() {
-            synchronized (mLock) {
-                // If we are still the last one to be scheduled, remove ourselves.
-                if (mLoaderTask == mTask) {
-                    mLoaderTask = null;
-                }
-                mIsLoaderTaskRunning = false;
-            }
-        }
-    }
-
-    public LoaderTransaction beginLoader(@NonNull final LoaderTask task)
-            throws CancellationException {
-        return new LoaderTransaction(task);
-    }
-
-    /**
-     * Refreshes the cached shortcuts if the shortcut permission has changed.
-     * Current implementation simply reloads the workspace, but it can be optimized to
-     * use partial updates similar to {@link UserCache}
-     */
-    public void validateModelDataOnResume() {
-        MODEL_EXECUTOR.getHandler().removeCallbacks(mDataValidationCheck);
-        MODEL_EXECUTOR.post(mDataValidationCheck);
-    }
-
-    /**
-     * Called when the icons for packages have been updated in the icon cache.
-     */
-    public void onPackageIconsUpdated(@NonNull final HashSet<String> updatedPackages,
-            @NonNull final UserHandle user) {
-        // If any package icon has changed (app was updated while launcher was dead),
-        // update the corresponding shortcuts.
-        enqueueModelUpdateTask(new CacheDataUpdatedTask(
-                CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages));
-    }
-
-    /**
-     * Called when the labels for the widgets has updated in the icon cache.
-     */
-    public void onWidgetLabelsUpdated(@NonNull final HashSet<String> updatedPackages,
-            @NonNull final UserHandle user) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) ->  {
-            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, mApp);
-            taskController.bindUpdatedWidgets(dataModel);
-        });
-    }
-
-    public void enqueueModelUpdateTask(@NonNull final ModelUpdateTask task) {
-        if (mModelDestroyed) {
-            return;
-        }
-        MODEL_EXECUTOR.execute(() -> {
-            if (!isModelLoaded()) {
-                // Loader has not yet run.
-                return;
-            }
-            ModelTaskController controller = new ModelTaskController(
-                    mApp, mBgDataModel, mBgAllAppsList, this, MAIN_EXECUTOR);
-            task.execute(controller, mBgDataModel, mBgAllAppsList);
-        });
-    }
-
-    /**
-     * A task to be executed on the current callbacks on the UI thread.
-     * If there is no current callbacks, the task is ignored.
-     */
-    public interface CallbackTask {
-
-        void execute(@NonNull Callbacks callbacks);
-    }
-
-    public interface ModelUpdateTask {
-
-        void execute(@NonNull ModelTaskController taskController,
-                @NonNull BgDataModel dataModel, @NonNull AllAppsList apps);
-    }
-
-    public void updateAndBindWorkspaceItem(@NonNull final WorkspaceItemInfo si,
-            @NonNull final ShortcutInfo info) {
-        updateAndBindWorkspaceItem(() -> {
-            si.updateFromDeepShortcutInfo(info, mApp.getContext());
-            mApp.getIconCache().getShortcutIcon(si, info);
-            return si;
-        });
-    }
-
-    /**
-     * Utility method to update a shortcut on the background thread.
-     */
-    public void updateAndBindWorkspaceItem(
-            @NonNull final Supplier<WorkspaceItemInfo> itemProvider) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) ->  {
-            WorkspaceItemInfo info = itemProvider.get();
-            taskController.getModelWriter().updateItemInDatabase(info);
-            ArrayList<WorkspaceItemInfo> update = new ArrayList<>();
-            update.add(info);
-            taskController.bindUpdatedWorkspaceItems(update);
-        });
-    }
-
-    public void refreshAndBindWidgetsAndShortcuts(@Nullable final PackageUserKey packageUser) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) ->  {
-            dataModel.widgetsModel.update(taskController.getApp(), packageUser);
-            taskController.bindUpdatedWidgets(dataModel);
-        });
-    }
-
-    public void dumpState(@Nullable final String prefix, @Nullable final FileDescriptor fd,
-            @NonNull final PrintWriter writer, @NonNull final String[] args) {
-        if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
-            writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size());
-            for (AppInfo info : mBgAllAppsList.data) {
-                writer.println(prefix + "   title=\"" + info.title
-                        + "\" bitmapIcon=" + info.bitmap.icon
-                        + " componentName=" + info.componentName.getPackageName());
-            }
-            writer.println();
-        }
-        mModelDelegate.dump(prefix, fd, writer, args);
-        mBgDataModel.dump(prefix, fd, writer, args);
-    }
-
-    /**
-     * Returns true if there are any callbacks attached to the model
-     */
-    public boolean hasCallbacks() {
-        synchronized (mCallbacksList) {
-            return !mCallbacksList.isEmpty();
-        }
-    }
-
-    /**
-     * Returns an array of currently attached callbacks
-     */
-    @NonNull
-    public Callbacks[] getCallbacks() {
-        synchronized (mCallbacksList) {
-            return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
-        }
-    }
-
-    /**
-     * Returns the ID for the last model load. If the load ID doesn't match for a transaction, the
-     * transaction should be ignored.
-     */
-    public int getLastLoadId() {
-        return mLastLoadId;
-    }
-}
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
new file mode 100644
index 0000000..b56df46
--- /dev/null
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3
+
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import android.text.TextUtils
+import android.util.Log
+import android.util.Pair
+import androidx.annotation.WorkerThread
+import com.android.launcher3.celllayout.CellPosMapper
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.AddWorkspaceItemsTask
+import com.android.launcher3.model.AllAppsList
+import com.android.launcher3.model.BaseLauncherBinder
+import com.android.launcher3.model.BgDataModel
+import com.android.launcher3.model.CacheDataUpdatedTask
+import com.android.launcher3.model.ItemInstallQueue
+import com.android.launcher3.model.LoaderTask
+import com.android.launcher3.model.ModelDbController
+import com.android.launcher3.model.ModelDelegate
+import com.android.launcher3.model.ModelLauncherCallbacks
+import com.android.launcher3.model.ModelTaskController
+import com.android.launcher3.model.ModelWriter
+import com.android.launcher3.model.PackageUpdatedTask
+import com.android.launcher3.model.ReloadStringCacheTask
+import com.android.launcher3.model.ShortcutsChangedTask
+import com.android.launcher3.model.UserLockStateChangedTask
+import com.android.launcher3.model.WidgetsFilterDataProvider
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutRequest
+import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.Preconditions
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.util.concurrent.CancellationException
+import java.util.function.Consumer
+
+/**
+ * Maintains in-memory state of the Launcher. It is expected that there should be only one
+ * LauncherModel object held in a static. Also provide APIs for updating the database state for the
+ * Launcher.
+ */
+class LauncherModel(
+    private val context: Context,
+    private val mApp: LauncherAppState,
+    private val iconCache: IconCache,
+    private val widgetsFilterDataProvider: WidgetsFilterDataProvider,
+    appFilter: AppFilter,
+    mPmHelper: PackageManagerHelper,
+    isPrimaryInstance: Boolean,
+) {
+
+    private val mCallbacksList = ArrayList<BgDataModel.Callbacks>(1)
+
+    // < only access in worker thread >
+    private val mBgAllAppsList = AllAppsList(iconCache, appFilter)
+
+    /**
+     * All the static data should be accessed on the background thread, A lock should be acquired on
+     * this object when accessing any data from this model.
+     */
+    private val mBgDataModel = BgDataModel()
+
+    val modelDelegate: ModelDelegate =
+        ModelDelegate.newInstance(
+            context,
+            mApp,
+            mPmHelper,
+            mBgAllAppsList,
+            mBgDataModel,
+            isPrimaryInstance,
+        )
+
+    val modelDbController = ModelDbController(context)
+
+    private val mLock = Any()
+
+    private var mLoaderTask: LoaderTask? = null
+    private var mIsLoaderTaskRunning = false
+
+    // only allow this once per reboot to reload work apps
+    private var mShouldReloadWorkProfile = true
+
+    // Indicates whether the current model data is valid or not.
+    // We start off with everything not loaded. After that, we assume that
+    // our monitoring of the package manager provides all updates and we never
+    // need to do a requery. This is only ever touched from the loader thread.
+    private var mModelLoaded = false
+    private var mModelDestroyed = false
+
+    fun isModelLoaded() =
+        synchronized(mLock) { mModelLoaded && mLoaderTask == null && !mModelDestroyed }
+
+    /**
+     * Returns the ID for the last model load. If the load ID doesn't match for a transaction, the
+     * transaction should be ignored.
+     */
+    var lastLoadId: Int = -1
+        private set
+
+    // Runnable to check if the shortcuts permission has changed.
+    private val mDataValidationCheck = Runnable {
+        if (mModelLoaded) {
+            modelDelegate.validateData()
+        }
+    }
+
+    fun newModelCallbacks() = ModelLauncherCallbacks(this::enqueueModelUpdateTask)
+
+    /** Adds the provided items to the workspace. */
+    fun addAndBindAddedWorkspaceItems(itemList: List<Pair<ItemInfo?, Any?>?>) {
+        callbacks.forEach { it.preAddApps() }
+        enqueueModelUpdateTask(AddWorkspaceItemsTask(itemList))
+    }
+
+    fun getWriter(
+        verifyChanges: Boolean,
+        cellPosMapper: CellPosMapper?,
+        owner: BgDataModel.Callbacks?,
+    ) = ModelWriter(mApp.context, this, mBgDataModel, verifyChanges, cellPosMapper, owner)
+
+    /** Returns the [WidgetsFilterDataProvider] that manages widget filters. */
+    fun getWidgetsFilterDataProvider(): WidgetsFilterDataProvider {
+        return widgetsFilterDataProvider
+    }
+
+    /** Called when the icon for an app changes, outside of package event */
+    @WorkerThread
+    fun onAppIconChanged(packageName: String, user: UserHandle) {
+        // Update the icon for the calendar package
+        enqueueModelUpdateTask(PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageName))
+        ShortcutRequest(context, user).forPackage(packageName).query(ShortcutRequest.PINNED).let {
+            if (it.isNotEmpty()) {
+                enqueueModelUpdateTask(ShortcutsChangedTask(packageName, it, user, false))
+            }
+        }
+    }
+
+    /** Called when the workspace items have drastically changed */
+    fun onWorkspaceUiChanged() {
+        MODEL_EXECUTOR.execute(modelDelegate::workspaceLoadComplete)
+    }
+
+    /** Called when the model is destroyed */
+    fun destroy() {
+        mModelDestroyed = true
+        MODEL_EXECUTOR.execute {
+            modelDelegate.destroy()
+            widgetsFilterDataProvider.destroy()
+        }
+    }
+
+    fun onBroadcastIntent(intent: Intent) {
+        if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
+        when (intent.action) {
+            Intent.ACTION_LOCALE_CHANGED,
+            LauncherAppState.ACTION_FORCE_ROLOAD ->
+                // If we have changed locale we need to clear out the labels in all apps/workspace.
+                forceReload()
+            DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED ->
+                enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
+        }
+    }
+
+    /**
+     * Called then there use a user event
+     *
+     * @see UserCache.addUserEventListener
+     */
+    fun onUserEvent(user: UserHandle, action: String) {
+        when (action) {
+            Intent.ACTION_MANAGED_PROFILE_AVAILABLE -> {
+                if (mShouldReloadWorkProfile) {
+                    forceReload()
+                } else {
+                    enqueueModelUpdateTask(
+                        PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)
+                    )
+                }
+                mShouldReloadWorkProfile = false
+            }
+            Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE -> {
+                mShouldReloadWorkProfile = false
+                enqueueModelUpdateTask(
+                    PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)
+                )
+            }
+            UserCache.ACTION_PROFILE_LOCKED ->
+                enqueueModelUpdateTask(UserLockStateChangedTask(user, false))
+            UserCache.ACTION_PROFILE_UNLOCKED ->
+                enqueueModelUpdateTask(UserLockStateChangedTask(user, true))
+            Intent.ACTION_MANAGED_PROFILE_REMOVED -> {
+                LauncherPrefs.get(mApp.context).put(LauncherPrefs.WORK_EDU_STEP, 0)
+                forceReload()
+            }
+            UserCache.ACTION_PROFILE_ADDED,
+            UserCache.ACTION_PROFILE_REMOVED -> forceReload()
+            UserCache.ACTION_PROFILE_AVAILABLE,
+            UserCache.ACTION_PROFILE_UNAVAILABLE -> {
+                // This broadcast is only available when android.os.Flags.allowPrivateProfile() is
+                // set. For Work-profile this broadcast will be sent in addition to
+                // ACTION_MANAGED_PROFILE_AVAILABLE/UNAVAILABLE. So effectively, this if block only
+                // handles the non-work profile case.
+                enqueueModelUpdateTask(
+                    PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)
+                )
+            }
+        }
+    }
+
+    /**
+     * Reloads the workspace items from the DB and re-binds the workspace. This should generally not
+     * be called as DB updates are automatically followed by UI update
+     */
+    fun forceReload() {
+        synchronized(mLock) {
+            // Stop any existing loaders first, so they don't set mModelLoaded to true later
+            stopLoader()
+            mModelLoaded = false
+        }
+        rebindCallbacks()
+    }
+
+    /** Rebinds all existing callbacks with already loaded model */
+    fun rebindCallbacks() {
+        if (hasCallbacks()) {
+            startLoader()
+        }
+    }
+
+    /** Removes an existing callback */
+    fun removeCallbacks(callbacks: BgDataModel.Callbacks) {
+        synchronized(mCallbacksList) {
+            Preconditions.assertUIThread()
+            if (mCallbacksList.remove(callbacks)) {
+                if (stopLoader()) {
+                    // Rebind existing callbacks
+                    startLoader()
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     *
+     * @return true if workspace load was performed synchronously
+     */
+    fun addCallbacksAndLoad(callbacks: BgDataModel.Callbacks): Boolean {
+        synchronized(mLock) {
+            addCallbacks(callbacks)
+            return startLoader(arrayOf(callbacks))
+        }
+    }
+
+    /** Adds a callbacks to receive model updates */
+    fun addCallbacks(callbacks: BgDataModel.Callbacks) {
+        Preconditions.assertUIThread()
+        synchronized(mCallbacksList) { mCallbacksList.add(callbacks) }
+    }
+
+    /**
+     * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
+     *
+     * @return true if the page could be bound synchronously.
+     */
+    fun startLoader() = startLoader(arrayOf())
+
+    private fun startLoader(newCallbacks: Array<BgDataModel.Callbacks>): Boolean {
+        // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
+        ItemInstallQueue.INSTANCE.get(context).pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING)
+        synchronized(mLock) {
+            // If there is already one running, tell it to stop.
+            val wasRunning = stopLoader()
+            val bindDirectly = mModelLoaded && !mIsLoaderTaskRunning
+            val bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.isEmpty()
+            val callbacksList = if (bindAllCallbacks) callbacks else newCallbacks
+            if (callbacksList.isNotEmpty()) {
+                // Clear any pending bind-runnables from the synchronized load process.
+                callbacksList.forEach { MAIN_EXECUTOR.execute(it::clearPendingBinds) }
+
+                val launcherBinder =
+                    BaseLauncherBinder(mApp, mBgDataModel, mBgAllAppsList, callbacksList)
+                if (bindDirectly) {
+                    // Divide the set of loaded items into those that we are binding synchronously,
+                    // and everything else that is to be bound normally (asynchronously).
+                    launcherBinder.bindWorkspace(bindAllCallbacks, /* isBindSync= */ true)
+                    // For now, continue posting the binding of AllApps as there are other
+                    // issues that arise from that.
+                    launcherBinder.bindAllApps()
+                    launcherBinder.bindDeepShortcuts()
+                    launcherBinder.bindWidgets()
+                    return true
+                } else {
+                    mLoaderTask =
+                        LoaderTask(
+                            mApp,
+                            mBgAllAppsList,
+                            mBgDataModel,
+                            this.modelDelegate,
+                            launcherBinder,
+                            widgetsFilterDataProvider,
+                        )
+
+                    // Always post the loader task, instead of running directly
+                    // (even on same thread) so that we exit any nested synchronized blocks
+                    MODEL_EXECUTOR.post(mLoaderTask)
+                }
+            }
+        }
+        return false
+    }
+
+    /**
+     * If there is already a loader task running, tell it to stop.
+     *
+     * @return true if an existing loader was stopped.
+     */
+    private fun stopLoader(): Boolean {
+        synchronized(mLock) {
+            val oldTask: LoaderTask? = mLoaderTask
+            mLoaderTask = null
+            if (oldTask != null) {
+                oldTask.stopLocked()
+                return true
+            }
+            return false
+        }
+    }
+
+    /**
+     * Loads the model if not loaded
+     *
+     * @param callback called with the data model upon successful load or null on model thread.
+     */
+    fun loadAsync(callback: Consumer<BgDataModel?>) {
+        synchronized(mLock) {
+            if (!mModelLoaded && !mIsLoaderTaskRunning) {
+                startLoader()
+            }
+        }
+        MODEL_EXECUTOR.post { callback.accept(if (isModelLoaded()) mBgDataModel else null) }
+    }
+
+    inner class LoaderTransaction(task: LoaderTask) : AutoCloseable {
+        private var mTask: LoaderTask? = null
+
+        init {
+            synchronized(mLock) {
+                if (mLoaderTask !== task) {
+                    throw CancellationException("Loader already stopped")
+                }
+                this@LauncherModel.lastLoadId++
+                mTask = task
+                mIsLoaderTaskRunning = true
+                mModelLoaded = false
+            }
+        }
+
+        fun commit() {
+            synchronized(mLock) {
+                // Everything loaded bind the data.
+                mModelLoaded = true
+            }
+        }
+
+        override fun close() {
+            synchronized(mLock) {
+                // If we are still the last one to be scheduled, remove ourselves.
+                if (mLoaderTask === mTask) {
+                    mLoaderTask = null
+                }
+                mIsLoaderTaskRunning = false
+            }
+        }
+    }
+
+    @Throws(CancellationException::class)
+    fun beginLoader(task: LoaderTask) = LoaderTransaction(task)
+
+    /**
+     * Refreshes the cached shortcuts if the shortcut permission has changed. Current implementation
+     * simply reloads the workspace, but it can be optimized to use partial updates similar to
+     * [UserCache]
+     */
+    fun validateModelDataOnResume() {
+        MODEL_EXECUTOR.handler.removeCallbacks(mDataValidationCheck)
+        MODEL_EXECUTOR.post(mDataValidationCheck)
+    }
+
+    /** Called when the icons for packages have been updated in the icon cache. */
+    fun onPackageIconsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) {
+        // If any package icon has changed (app was updated while launcher was dead),
+        // update the corresponding shortcuts.
+        enqueueModelUpdateTask(
+            CacheDataUpdatedTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages)
+        )
+    }
+
+    /** Called when the labels for the widgets has updated in the icon cache. */
+    fun onWidgetLabelsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) {
+        enqueueModelUpdateTask { taskController, dataModel, _ ->
+            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, mApp)
+            taskController.bindUpdatedWidgets(dataModel)
+        }
+    }
+
+    /** Called when the widget filters are refreshed and available to bind to the model. */
+    fun onWidgetFiltersLoaded() {
+        enqueueModelUpdateTask { taskController, dataModel, _ ->
+            dataModel.widgetsModel.updateWidgetFilters(widgetsFilterDataProvider)
+            taskController.bindUpdatedWidgets(dataModel)
+        }
+    }
+
+    fun enqueueModelUpdateTask(task: ModelUpdateTask) {
+        if (mModelDestroyed) {
+            return
+        }
+        MODEL_EXECUTOR.execute {
+            if (!isModelLoaded()) {
+                // Loader has not yet run.
+                return@execute
+            }
+            task.execute(
+                ModelTaskController(mApp, mBgDataModel, mBgAllAppsList, this, MAIN_EXECUTOR),
+                mBgDataModel,
+                mBgAllAppsList,
+            )
+        }
+    }
+
+    /**
+     * A task to be executed on the current callbacks on the UI thread. If there is no current
+     * callbacks, the task is ignored.
+     */
+    fun interface CallbackTask {
+        fun execute(callbacks: BgDataModel.Callbacks)
+    }
+
+    fun interface ModelUpdateTask {
+        fun execute(taskController: ModelTaskController, dataModel: BgDataModel, apps: AllAppsList)
+    }
+
+    fun updateAndBindWorkspaceItem(si: WorkspaceItemInfo, info: ShortcutInfo) {
+        enqueueModelUpdateTask { taskController, _, _ ->
+            si.updateFromDeepShortcutInfo(info, context)
+            iconCache.getShortcutIcon(si, info)
+            taskController.getModelWriter().updateItemInDatabase(si)
+            taskController.bindUpdatedWorkspaceItems(listOf(si))
+        }
+    }
+
+    fun refreshAndBindWidgetsAndShortcuts(packageUser: PackageUserKey?) {
+        enqueueModelUpdateTask { taskController, dataModel, _ ->
+            dataModel.widgetsModel.update(taskController.app, packageUser)
+            taskController.bindUpdatedWidgets(dataModel)
+        }
+    }
+
+    fun dumpState(prefix: String?, fd: FileDescriptor?, writer: PrintWriter, args: Array<String?>) {
+        if (args.isNotEmpty() && TextUtils.equals(args[0], "--all")) {
+            writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size)
+            for (info in mBgAllAppsList.data) {
+                writer.println(
+                    "$prefix   title=\"${info.title}\" bitmapIcon=${info.bitmap.icon} componentName=${info.targetPackage}"
+                )
+            }
+            writer.println()
+        }
+        modelDelegate.dump(prefix, fd, writer, args)
+        mBgDataModel.dump(prefix, fd, writer, args)
+    }
+
+    /** Returns true if there are any callbacks attached to the model */
+    fun hasCallbacks() = synchronized(mCallbacksList) { mCallbacksList.isNotEmpty() }
+
+    /** Returns an array of currently attached callbacks */
+    val callbacks: Array<BgDataModel.Callbacks>
+        get() {
+            synchronized(mCallbacksList) {
+                return mCallbacksList.toTypedArray<BgDataModel.Callbacks>()
+            }
+        }
+
+    companion object {
+        private const val DEBUG_RECEIVER = false
+
+        const val TAG = "Launcher.Model"
+    }
+}
diff --git a/src/com/android/launcher3/LauncherPrefChangeListener.java b/src/com/android/launcher3/LauncherPrefChangeListener.java
new file mode 100644
index 0000000..3e9a846
--- /dev/null
+++ b/src/com/android/launcher3/LauncherPrefChangeListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+
+/**
+ * Listener for changes in [LauncherPrefs].
+ * <p>
+ * The listener also serves as an [OnSharedPreferenceChangeListener] where
+ * [onSharedPreferenceChanged] delegates to [onPrefChanged]. Overriding [onSharedPreferenceChanged]
+ * breaks compatibility with [SharedPreferences].
+ */
+public interface LauncherPrefChangeListener extends OnSharedPreferenceChangeListener {
+
+    /** Callback invoked when the preference for [key] has changed. */
+    void onPrefChanged(String key);
+
+    @Override
+    default void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+        onPrefChanged(key);
+    }
+}
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 13181e8..712c56c 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -18,7 +18,6 @@
 import android.content.Context
 import android.content.Context.MODE_PRIVATE
 import android.content.SharedPreferences
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN
 import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY
@@ -27,6 +26,7 @@
 import com.android.launcher3.pm.InstallSessionHelper
 import com.android.launcher3.provider.RestoreDbTask
 import com.android.launcher3.provider.RestoreDbTask.FIRST_LOAD_AFTER_RESTORE_KEY
+import com.android.launcher3.settings.SettingsActivity
 import com.android.launcher3.states.RotationHelper
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.MainThreadInitializedObject
@@ -34,11 +34,184 @@
 import com.android.launcher3.util.Themes
 
 /**
- * Use same context for shared preferences, so that we use a single cached instance
+ * Manages Launcher [SharedPreferences] through [Item] instances.
  *
  * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
  */
-class LauncherPrefs(private val encryptedContext: Context) : SafeCloseable {
+abstract class LauncherPrefs : SafeCloseable {
+
+    /** Returns the value with type [T] for [item]. */
+    abstract fun <T> get(item: ContextualItem<T>): T
+
+    /** Returns the value with type [T] for [item]. */
+    abstract fun <T> get(item: ConstantItem<T>): T
+
+    /** Stores the values for each item in preferences. */
+    abstract fun put(vararg itemsToValues: Pair<Item, Any>)
+
+    /** Stores the [value] with type [T] for [item] in preferences. */
+    abstract fun <T : Any> put(item: Item, value: T)
+
+    /** Synchronous version of [put]. */
+    abstract fun putSync(vararg itemsToValues: Pair<Item, Any>)
+
+    /** Registers [listener] for [items]. */
+    abstract fun addListener(listener: LauncherPrefChangeListener, vararg items: Item)
+
+    /** Unregisters [listener] for [items]. */
+    abstract fun removeListener(listener: LauncherPrefChangeListener, vararg items: Item)
+
+    /** Returns `true` iff all [items] have a value. */
+    abstract fun has(vararg items: Item): Boolean
+
+    /** Removes the value for each item in [items]. */
+    abstract fun remove(vararg items: Item)
+
+    /** Synchronous version of [remove]. */
+    abstract fun removeSync(vararg items: Item)
+
+    companion object {
+        @VisibleForTesting const val BOOT_AWARE_PREFS_KEY = "boot_aware_prefs"
+
+        @JvmField
+        var INSTANCE = MainThreadInitializedObject<LauncherPrefs> { LauncherPrefsImpl(it) }
+
+        @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
+
+        const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
+        const val TASKBAR_PINNING_DESKTOP_MODE_KEY = "TASKBAR_PINNING_DESKTOP_MODE_KEY"
+        const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY"
+        @JvmField
+        val ICON_STATE = nonRestorableItem("pref_icon_shape_path", "", EncryptionType.ENCRYPTED)
+
+        @JvmField
+        val ENABLE_TWOLINE_ALLAPPS_TOGGLE = backedUpItem("pref_enable_two_line_toggle", false)
+        @JvmField
+        val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED)
+        @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
+        @JvmField val WORK_EDU_STEP = backedUpItem("showed_work_profile_edu", 0)
+        @JvmField
+        val WORKSPACE_SIZE =
+            backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", EncryptionType.ENCRYPTED)
+        @JvmField
+        val HOTSEAT_COUNT =
+            backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, EncryptionType.ENCRYPTED)
+        @JvmField
+        val TASKBAR_PINNING =
+            backedUpItem(TASKBAR_PINNING_KEY, false, EncryptionType.DEVICE_PROTECTED)
+        @JvmField
+        val TASKBAR_PINNING_IN_DESKTOP_MODE =
+            backedUpItem(TASKBAR_PINNING_DESKTOP_MODE_KEY, true, EncryptionType.DEVICE_PROTECTED)
+
+        @JvmField
+        val DEVICE_TYPE =
+            backedUpItem(
+                DeviceGridState.KEY_DEVICE_TYPE,
+                InvariantDeviceProfile.TYPE_PHONE,
+                EncryptionType.ENCRYPTED,
+            )
+        @JvmField
+        val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", EncryptionType.ENCRYPTED)
+        @JvmField
+        val SHOULD_SHOW_SMARTSPACE =
+            backedUpItem(
+                SHOULD_SHOW_SMARTSPACE_KEY,
+                WIDGET_ON_FIRST_SCREEN,
+                EncryptionType.DEVICE_PROTECTED,
+            )
+        @JvmField
+        val RESTORE_DEVICE =
+            backedUpItem(
+                RestoreDbTask.RESTORED_DEVICE_TYPE,
+                InvariantDeviceProfile.TYPE_PHONE,
+                EncryptionType.ENCRYPTED,
+            )
+        @JvmField
+        val NO_DB_FILES_RESTORED =
+            nonRestorableItem("no_db_files_restored", false, EncryptionType.DEVICE_PROTECTED)
+        @JvmField
+        val IS_FIRST_LOAD_AFTER_RESTORE =
+            nonRestorableItem(FIRST_LOAD_AFTER_RESTORE_KEY, false, EncryptionType.ENCRYPTED)
+        @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
+        @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
+
+        @JvmField
+        val GRID_NAME =
+            ConstantItem(
+                "idp_grid_name",
+                isBackedUp = true,
+                defaultValue = null,
+                encryptionType = EncryptionType.ENCRYPTED,
+                type = String::class.java,
+            )
+        @JvmField
+        val ALLOW_ROTATION =
+            backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
+                RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
+            }
+
+        @JvmField
+        val FIXED_LANDSCAPE_MODE = backedUpItem(SettingsActivity.FIXED_LANDSCAPE_MODE, false)
+
+        // Preferences for widget configurations
+        @JvmField
+        val RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
+            backedUpItem("launcher.reconfigurable_widget_education_tip_seen", false)
+
+        @JvmStatic
+        fun <T> backedUpItem(
+            sharedPrefKey: String,
+            defaultValue: T,
+            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
+        ): ConstantItem<T> =
+            ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, encryptionType)
+
+        @JvmStatic
+        fun <T> backedUpItem(
+            sharedPrefKey: String,
+            type: Class<out T>,
+            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
+            defaultValueFromContext: (c: Context) -> T,
+        ): ContextualItem<T> =
+            ContextualItem(
+                sharedPrefKey,
+                isBackedUp = true,
+                defaultValueFromContext,
+                encryptionType,
+                type,
+            )
+
+        @JvmStatic
+        fun <T> nonRestorableItem(
+            sharedPrefKey: String,
+            defaultValue: T,
+            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
+        ): ConstantItem<T> =
+            ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, encryptionType)
+
+        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
+        @JvmStatic
+        fun getPrefs(context: Context): SharedPreferences {
+            // Use application context for shared preferences, so we use single cached instance
+            return context.applicationContext.getSharedPreferences(
+                SHARED_PREFERENCES_KEY,
+                MODE_PRIVATE,
+            )
+        }
+
+        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
+        @JvmStatic
+        fun getDevicePrefs(context: Context): SharedPreferences {
+            // Use application context for shared preferences, so we use a single cached instance
+            return context.applicationContext.getSharedPreferences(
+                DEVICE_PREFERENCES_KEY,
+                MODE_PRIVATE,
+            )
+        }
+    }
+}
+
+private class LauncherPrefsImpl(private val encryptedContext: Context) : LauncherPrefs() {
     private val deviceProtectedStorageContext =
         encryptedContext.createDeviceProtectedStorageContext()
 
@@ -54,11 +227,11 @@
         else item.encryptedPrefs
 
     /** Wrapper around `getInner` for a `ContextualItem` */
-    fun <T> get(item: ContextualItem<T>): T =
+    override fun <T> get(item: ContextualItem<T>): T =
         getInner(item, item.defaultValueFromContext(encryptedContext))
 
     /** Wrapper around `getInner` for an `Item` */
-    fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
+    override fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
 
     /**
      * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
@@ -97,17 +270,17 @@
      * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the
      * provided item configurations.
      */
-    fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
+    override fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
         prepareToPutValues(itemsToValues).forEach { it.apply() }
 
     /** See referenced `put` method above. */
-    fun <T : Any> put(item: Item, value: T): Unit = put(item.to(value))
+    override fun <T : Any> put(item: Item, value: T): Unit = put(item.to(value))
 
     /**
      * Synchronously stores all the values provided according to their associated Item
      * configuration.
      */
-    fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
+    override fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
         prepareToPutValues(itemsToValues).forEach { it.commit() }
 
     /**
@@ -152,7 +325,7 @@
     @Suppress("UNCHECKED_CAST")
     private fun SharedPreferences.Editor.putValue(
         item: Item,
-        value: Any?
+        value: Any?,
     ): SharedPreferences.Editor =
         when (item.type) {
             String::class.java -> putString(item.sharedPrefKey, value as? String)
@@ -176,7 +349,7 @@
      * `SharedPreferences` files associated with the provided list of items. The listener will need
      * to filter update notifications so they don't activate for non-relevant updates.
      */
-    fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
+    override fun addListener(listener: LauncherPrefChangeListener, vararg items: Item) {
         items
             .map { chooseSharedPreferences(it) }
             .distinct()
@@ -187,7 +360,7 @@
      * Stops the listener from getting notified of any more updates to any of the
      * `SharedPreferences` files associated with any of the provided list of [Item].
      */
-    fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
+    override fun removeListener(listener: LauncherPrefChangeListener, vararg items: Item) {
         // If a listener is not registered to a SharedPreference, unregistering it does nothing
         items
             .map { chooseSharedPreferences(it) }
@@ -199,7 +372,7 @@
      * Checks if all the provided [Item] have values stored in their corresponding
      * `SharedPreferences` files.
      */
-    fun has(vararg items: Item): Boolean {
+    override fun has(vararg items: Item): Boolean {
         items
             .groupBy { chooseSharedPreferences(it) }
             .forEach { (prefs, itemsSublist) ->
@@ -211,10 +384,10 @@
     /**
      * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file.
      */
-    fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
+    override fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
 
     /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */
-    fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
+    override fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
 
     /**
      * Removes the key value pairs stored in `SharedPreferences` for each corresponding Item. If the
@@ -244,138 +417,6 @@
     }
 
     override fun close() {}
-
-    companion object {
-        @VisibleForTesting const val BOOT_AWARE_PREFS_KEY = "boot_aware_prefs"
-
-        @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) }
-
-        @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
-
-        const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
-        const val TASKBAR_PINNING_DESKTOP_MODE_KEY = "TASKBAR_PINNING_DESKTOP_MODE_KEY"
-        const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY"
-        @JvmField
-        val ICON_STATE = nonRestorableItem("pref_icon_shape_path", "", EncryptionType.ENCRYPTED)
-
-        @JvmField
-        val ENABLE_TWOLINE_ALLAPPS_TOGGLE = backedUpItem("pref_enable_two_line_toggle", false)
-        @JvmField
-        val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED)
-        @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
-        @JvmField val WORK_EDU_STEP = backedUpItem("showed_work_profile_edu", 0)
-        @JvmField
-        val WORKSPACE_SIZE =
-            backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", EncryptionType.ENCRYPTED)
-        @JvmField
-        val HOTSEAT_COUNT =
-            backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, EncryptionType.ENCRYPTED)
-        @JvmField
-        val TASKBAR_PINNING =
-            backedUpItem(TASKBAR_PINNING_KEY, false, EncryptionType.DEVICE_PROTECTED)
-        @JvmField
-        val TASKBAR_PINNING_IN_DESKTOP_MODE =
-            backedUpItem(TASKBAR_PINNING_DESKTOP_MODE_KEY, true, EncryptionType.DEVICE_PROTECTED)
-
-        @JvmField
-        val DEVICE_TYPE =
-            backedUpItem(
-                DeviceGridState.KEY_DEVICE_TYPE,
-                InvariantDeviceProfile.TYPE_PHONE,
-                EncryptionType.ENCRYPTED
-            )
-        @JvmField
-        val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", EncryptionType.ENCRYPTED)
-        @JvmField
-        val SHOULD_SHOW_SMARTSPACE =
-            backedUpItem(
-                SHOULD_SHOW_SMARTSPACE_KEY,
-                WIDGET_ON_FIRST_SCREEN,
-                EncryptionType.DEVICE_PROTECTED
-            )
-        @JvmField
-        val RESTORE_DEVICE =
-            backedUpItem(
-                RestoreDbTask.RESTORED_DEVICE_TYPE,
-                InvariantDeviceProfile.TYPE_PHONE,
-                EncryptionType.ENCRYPTED
-            )
-        @JvmField
-        val IS_FIRST_LOAD_AFTER_RESTORE =
-            nonRestorableItem(FIRST_LOAD_AFTER_RESTORE_KEY, false, EncryptionType.ENCRYPTED)
-        @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
-        @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
-        @JvmField
-        val GRID_NAME =
-            ConstantItem(
-                "idp_grid_name",
-                isBackedUp = true,
-                defaultValue = null,
-                encryptionType = EncryptionType.ENCRYPTED,
-                type = String::class.java
-            )
-        @JvmField
-        val ALLOW_ROTATION =
-            backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
-                RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
-            }
-
-        // Preferences for widget configurations
-        @JvmField
-        val RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
-            backedUpItem("launcher.reconfigurable_widget_education_tip_seen", false)
-
-        @JvmStatic
-        fun <T> backedUpItem(
-            sharedPrefKey: String,
-            defaultValue: T,
-            encryptionType: EncryptionType = EncryptionType.ENCRYPTED
-        ): ConstantItem<T> =
-            ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, encryptionType)
-
-        @JvmStatic
-        fun <T> backedUpItem(
-            sharedPrefKey: String,
-            type: Class<out T>,
-            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
-            defaultValueFromContext: (c: Context) -> T
-        ): ContextualItem<T> =
-            ContextualItem(
-                sharedPrefKey,
-                isBackedUp = true,
-                defaultValueFromContext,
-                encryptionType,
-                type
-            )
-
-        @JvmStatic
-        fun <T> nonRestorableItem(
-            sharedPrefKey: String,
-            defaultValue: T,
-            encryptionType: EncryptionType = EncryptionType.ENCRYPTED
-        ): ConstantItem<T> =
-            ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, encryptionType)
-
-        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
-        @JvmStatic
-        fun getPrefs(context: Context): SharedPreferences {
-            // Use application context for shared preferences, so we use single cached instance
-            return context.applicationContext.getSharedPreferences(
-                SHARED_PREFERENCES_KEY,
-                MODE_PRIVATE
-            )
-        }
-
-        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
-        @JvmStatic
-        fun getDevicePrefs(context: Context): SharedPreferences {
-            // Use application context for shared preferences, so we use a single cached instance
-            return context.applicationContext.getSharedPreferences(
-                DEVICE_PREFERENCES_KEY,
-                MODE_PRIVATE
-            )
-        }
-    }
 }
 
 abstract class Item {
@@ -395,7 +436,7 @@
     val defaultValue: T,
     override val encryptionType: EncryptionType,
     // The default value can be null. If so, the type needs to be explicitly stated, or else NPE
-    override val type: Class<out T> = defaultValue!!::class.java
+    override val type: Class<out T> = defaultValue!!::class.java,
 ) : Item() {
 
     fun get(c: Context): T = LauncherPrefs.get(c).get(this)
@@ -406,7 +447,7 @@
     override val isBackedUp: Boolean,
     private val defaultSupplier: (c: Context) -> T,
     override val encryptionType: EncryptionType,
-    override val type: Class<out T>
+    override val type: Class<out T>,
 ) : Item() {
     private var default: T? = null
 
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 7176733..d645734 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -11,6 +11,7 @@
 
 import com.android.launcher3.graphics.SysUiScrim;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.util.window.WindowManagerProxy;
 
 import java.util.Collections;
@@ -20,7 +21,7 @@
 
     private final Rect mTempRect = new Rect();
 
-    private final StatefulActivity mActivity;
+    private final StatefulContainer mStatefulContainer;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
@@ -36,24 +37,25 @@
 
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mActivity = StatefulActivity.fromContext(context);
+        mStatefulContainer = StatefulContainer.fromContext(context);
         mSysUiScrim = new SysUiScrim(this);
     }
 
     private void handleSystemWindowInsets(Rect insets) {
         // Update device profile before notifying the children.
-        mActivity.getDeviceProfile().updateInsets(insets);
+        mStatefulContainer.getDeviceProfile().updateInsets(insets);
         boolean resetState = !insets.equals(mInsets);
         setInsets(insets);
 
         if (resetState) {
-            mActivity.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
+            mStatefulContainer.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
         }
     }
 
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mActivity.handleConfigurationChanged(mActivity.getResources().getConfiguration());
+        mStatefulContainer.handleConfigurationChanged(
+                mStatefulContainer.getContext().getResources().getConfiguration());
 
         insets = WindowManagerProxy.INSTANCE.get(getContext())
                 .normalizeWindowInsets(getContext(), insets, mTempRect);
@@ -72,7 +74,7 @@
     }
 
     public void dispatchInsets() {
-        mActivity.getDeviceProfile().updateInsets(mInsets);
+        mStatefulContainer.getDeviceProfile().updateInsets(mInsets);
         super.setInsets(mInsets);
     }
 
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 87ac193..1d2d161 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -16,8 +16,12 @@
 
 package com.android.launcher3;
 
+import static android.util.Base64.NO_PADDING;
+import static android.util.Base64.NO_WRAP;
+
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.BaseColumns;
+import android.util.Base64;
 
 import androidx.annotation.NonNull;
 
@@ -354,8 +358,17 @@
      * Launcher settings
      */
     public static final class Settings {
-        public static final String LAYOUT_DIGEST_KEY = "launcher3.layout.provider.blob";
+        public static final String LAYOUT_PROVIDER_KEY = "launcher3.layout.provider";
         public static final String LAYOUT_DIGEST_LABEL = "launcher-layout";
         public static final String LAYOUT_DIGEST_TAG = "ignore";
+        public static final String BLOB_KEY_PREFIX = "blob://";
+
+        /**
+         * Creates a key to be used for {@link #LAYOUT_PROVIDER_KEY}
+         * @param digest byte[] representing the message digest for the blob handle
+         */
+        public static String createBlobProviderKey(byte[] digest) {
+            return BLOB_KEY_PREFIX + Base64.encodeToString(digest, NO_WRAP | NO_PADDING);
+        }
     }
 }
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 102189b..7d5e481 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -375,8 +375,14 @@
     }
 
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
-        if ((this != NORMAL && this != HINT_STATE)
-                || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        boolean shouldFadeAdjacentScreens = (this == NORMAL || this == HINT_STATE)
+                && dp.shouldFadeAdjacentWorkspaceScreens();
+        // Avoid showing adjacent screens behind handheld All Apps sheet.
+        if (Flags.allAppsSheetForHandheld() && dp.isPhone && this == ALL_APPS) {
+            shouldFadeAdjacentScreens = true;
+        }
+        if (!shouldFadeAdjacentScreens) {
             return DEFAULT_ALPHA_PROVIDER;
         }
         final int centerPage = launcher.getWorkspace().getNextPage();
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 83c34ce..5d32525 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -61,7 +61,7 @@
         AbstractFloatingView.closeOpenViews(
             launcher,
             true,
-            AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
+            AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv(),
         )
         workspaceLoading = true
 
@@ -76,7 +76,7 @@
             TAG,
             "startBinding: " +
                 "hotseat layout was vertical: ${launcher.hotseat?.isHasVerticalHotseat}" +
-                " and is setting to ${launcher.deviceProfile.isVerticalBarLayout}"
+                " and is setting to ${launcher.deviceProfile.isVerticalBarLayout}",
         )
         launcher.hotseat?.resetLayout(launcher.deviceProfile.isVerticalBarLayout)
         TraceHelper.INSTANCE.endSection()
@@ -88,14 +88,12 @@
         pendingTasks: RunnableList,
         onCompleteSignal: RunnableList,
         workspaceItemCount: Int,
-        isBindSync: Boolean
+        isBindSync: Boolean,
     ) {
-        if (Utilities.ATLEAST_S) {
-            Trace.endAsyncSection(
-                TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME,
-                TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE
-            )
-        }
+        Trace.endAsyncSection(
+            TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME,
+            TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE,
+        )
         synchronouslyBoundPages = boundPages
         pagesToBindSynchronously = LIntSet()
         clearPendingBinds()
@@ -149,14 +147,14 @@
         // Cache one page worth of icons
         launcher.viewCache.setCacheSize(
             R.layout.folder_application,
-            deviceProfile.numFolderColumns * deviceProfile.numFolderRows
+            deviceProfile.numFolderColumns * deviceProfile.numFolderRows,
         )
         launcher.viewCache.setCacheSize(R.layout.folder_page, 2)
         TraceHelper.INSTANCE.endSection()
         launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true)
         launcher.workspace.pageIndicator.setPauseScroll(
             /*pause=*/ false,
-            deviceProfile.isTwoPanels
+            deviceProfile.isTwoPanels,
         )
         TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
     }
@@ -182,7 +180,7 @@
         val snackbar =
             AbstractFloatingView.getOpenView<AbstractFloatingView>(
                 launcher,
-                AbstractFloatingView.TYPE_SNACKBAR
+                AbstractFloatingView.TYPE_SNACKBAR,
             )
         snackbar?.post { snackbar.close(true) }
     }
@@ -191,7 +189,7 @@
     override fun bindAllApplications(
         apps: Array<AppInfo?>?,
         flags: Int,
-        packageUserKeytoUidMap: Map<PackageUserKey?, Int?>?
+        packageUserKeytoUidMap: Map<PackageUserKey?, Int?>?,
     ) {
         Preconditions.assertUIThread()
         val hadWorkApps = launcher.appsView.shouldShowTabs()
@@ -254,8 +252,11 @@
         PopupContainerWithArrow.dismissInvalidPopup(launcher)
     }
 
-    override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry?>?) {
-        launcher.popupDataProvider.allWidgets = allWidgets
+    override fun bindAllWidgets(
+        allWidgets: List<WidgetsListBaseEntry>,
+        defaultWidgets: List<WidgetsListBaseEntry>,
+    ) {
+        launcher.widgetPickerDataProvider.setWidgets(allWidgets, defaultWidgets)
     }
 
     /** Returns the ids of the workspaces to bind. */
@@ -304,14 +305,15 @@
         }
 
         val widgetsListBaseEntry: WidgetsListBaseEntry =
-            launcher.popupDataProvider.allWidgets.firstOrNull { item: WidgetsListBaseEntry ->
+            launcher.widgetPickerDataProvider.get().allWidgets.firstOrNull {
+                item: WidgetsListBaseEntry ->
                 item.mPkgItem.packageName == BuildConfig.APPLICATION_ID
             } ?: return
 
         val info =
             PendingAddWidgetInfo(
                 widgetsListBaseEntry.mWidgets[0].widgetInfo,
-                LauncherSettings.Favorites.CONTAINER_DESKTOP
+                LauncherSettings.Favorites.CONTAINER_DESKTOP,
             )
         launcher.addPendingItem(
             info,
@@ -319,14 +321,14 @@
             WorkspaceLayoutManager.FIRST_SCREEN_ID,
             intArrayOf(0, 0),
             info.spanX,
-            info.spanY
+            info.spanY,
         )
     }
 
     override fun bindScreens(orderedScreenIds: LIntArray) {
         launcher.workspace.pageIndicator.setPauseScroll(
             /*pause=*/ true,
-            launcher.deviceProfile.isTwoPanels
+            launcher.deviceProfile.isTwoPanels,
         )
         val firstScreenPosition = 0
         if (
@@ -353,7 +355,7 @@
     override fun bindAppsAdded(
         newScreens: LIntArray?,
         addNotAnimated: java.util.ArrayList<ItemInfo?>?,
-        addAnimated: java.util.ArrayList<ItemInfo?>?
+        addAnimated: java.util.ArrayList<ItemInfo?>?,
     ) {
         // Add the new screens
         if (newScreens != null) {
diff --git a/src/com/android/launcher3/MotionEventsUtils.java b/src/com/android/launcher3/MotionEventsUtils.java
index 3228ec6..fb244b0 100644
--- a/src/com/android/launcher3/MotionEventsUtils.java
+++ b/src/com/android/launcher3/MotionEventsUtils.java
@@ -18,8 +18,6 @@
 
 import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
-
 import android.annotation.TargetApi;
 import android.os.Build;
 import android.view.MotionEvent;
@@ -35,14 +33,12 @@
 
     @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static boolean isTrackpadScroll(MotionEvent event) {
-        return ENABLE_TRACKPAD_GESTURE.get()
-                && event.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE;
+        return event.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE;
     }
 
     @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static boolean isTrackpadMultiFingerSwipe(MotionEvent event) {
-        return ENABLE_TRACKPAD_GESTURE.get()
-                && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE;
+        return event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE;
     }
 
     public static boolean isTrackpadThreeFingerSwipe(MotionEvent event) {
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 365fbd3..0ec3b79 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1463,6 +1463,15 @@
                 mEdgeGlowLeft.onFlingVelocity(velocity);
                 mEdgeGlowRight.onFlingVelocity(velocity);
             }
+
+            // Detect if user tries to swipe to -1 page but gets disallowed by checking if there was
+            // left-over values in mEdgeGlowLeft (or mEdgeGlowRight in RLT).
+            final int layoutDir = getLayoutDirection();
+            if ((mEdgeGlowLeft.getDistance() > 0 && layoutDir == LAYOUT_DIRECTION_LTR)
+                    || (mEdgeGlowRight.getDistance() > 0 && layoutDir == LAYOUT_DIRECTION_RTL)) {
+                onDisallowSwipeToMinusOnePage();
+            }
+
             mEdgeGlowLeft.onRelease(ev);
             mEdgeGlowRight.onRelease(ev);
             // End any intermediate reordering states
@@ -1487,6 +1496,8 @@
         return true;
     }
 
+    protected void onDisallowSwipeToMinusOnePage() {}
+
     protected void onNotSnappingToPageInFreeScroll() { }
 
     /**
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 0a4fb73..b3cb948 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -7,7 +7,6 @@
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.INVALID;
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE;
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_UNINSTALL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_CANCELLED;
@@ -23,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;
@@ -36,7 +34,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.logging.InstanceId;
@@ -46,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;
@@ -242,8 +239,7 @@
 
     @Override
     public void completeDrop(final DragObject d) {
-        ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo,
-                d.logInstanceId);
+        ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo);
         mDropTargetHandler.onSecondaryTargetCompleteDrop(target, d);
     }
 
@@ -275,7 +271,7 @@
      * Performs the drop action and returns the target component for the dragObject or null if
      * the action was not performed.
      */
-    protected ComponentName performDropAction(View view, ItemInfo info, InstanceId instanceId) {
+    protected ComponentName performDropAction(View view, ItemInfo info) {
         if (mCurrentAccessibilityAction == RECONFIGURE) {
             int widgetId = getReconfigurableWidgetId(view);
             if (widgetId != INVALID_APPWIDGET_ID) {
@@ -283,21 +279,6 @@
             }
             return null;
         }
-        if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
-            if (FeatureFlags.ENABLE_DISMISS_PREDICTION_UNDO.get()) {
-                CharSequence announcement = getContext().getString(R.string.item_removed);
-                mDropTargetHandler
-                        .dismissPrediction(announcement, () -> {
-                        }, () -> {
-                            mStatsLogManager.logger()
-                                    .withInstanceId(instanceId)
-                                    .withItemInfo(info)
-                                    .log(LAUNCHER_DISMISS_PREDICTION_UNDO);
-                        });
-            }
-            return null;
-        }
-
         return performUninstall(getContext(), getUninstallTarget(getContext(), info), info);
     }
 
@@ -332,9 +313,8 @@
 
     @Override
     public void onAccessibilityDrop(View view, ItemInfo item) {
-        InstanceId instanceId = new InstanceIdSequence().newInstanceId();
-        doLog(instanceId, item);
-        performDropAction(view, item, instanceId);
+        doLog(new InstanceIdSequence().newInstanceId(), item);
+        performDropAction(view, item);
     }
 
     /**
@@ -361,9 +341,8 @@
         }
 
         public void onLauncherResume() {
-            // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well.
-            if (PackageManagerHelper.INSTANCE.get(mContext).getApplicationInfo(mPackageName,
-                    mDragObject.dragInfo.user, PackageManager.MATCH_UNINSTALLED_PACKAGES) == null) {
+            if (new ApplicationInfoWrapper(mContext, mPackageName, mDragObject.dragInfo.user)
+                    .getInfo() == null) {
                 mDragObject.dragSource = mOriginal;
                 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true);
                 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index d2c3c78..a8733f2 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -64,6 +64,7 @@
 
     private final ActivityContext mActivity;
     private boolean mInvertIfRtl = false;
+    public boolean mHasOnLayoutBeenCalled = false;
 
     @Nullable
     private TranslationProvider mTranslationProvider = null;
@@ -201,6 +202,7 @@
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         Trace.beginSection("ShortcutAndWidgetConteiner#onLayout");
+        mHasOnLayoutBeenCalled = true; // b/349929393 - is the required call to onLayout not done?
         int count = getChildCount();
         for (int i = 0; i < count; i++) {
             final View child = getChildAt(i);
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index a296f46..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;
@@ -106,8 +106,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.function.Predicate;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 /**
  * Various utilities shared amongst the Launcher's classes.
@@ -116,8 +114,7 @@
 
     private static final String TAG = "Launcher.Utilities";
 
-    private static final Pattern sTrimPattern =
-            Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$");
+    private static final String TRIM_PATTERN = "(^\\h+|\\h+$)";
 
     private static final Matrix sMatrix = new Matrix();
     private static final Matrix sInverseMatrix = new Matrix();
@@ -125,9 +122,6 @@
     public static final String[] EMPTY_STRING_ARRAY = new String[0];
     public static final Person[] EMPTY_PERSON_ARRAY = new Person[0];
 
-    @ChecksSdkIntAtLeast(api = VERSION_CODES.S)
-    public static final boolean ATLEAST_S = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
-
     @ChecksSdkIntAtLeast(api = VERSION_CODES.TIRAMISU, codename = "T")
     public static final boolean ATLEAST_T = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
 
@@ -181,6 +175,11 @@
         sIsRunningInTestHarness = true;
     }
 
+    /** Disables running test in test harness mode */
+    public static void disableRunningInTestHarnessForTests() {
+        sIsRunningInTestHarness = false;
+    }
+
     public static boolean isPropertyEnabled(String propertyName) {
         return Log.isLoggable(propertyName, Log.VERBOSE);
     }
@@ -421,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) {
@@ -445,10 +463,7 @@
         if (s == null) {
             return "";
         }
-
-        // Just strip any sequence of whitespace or java space characters from the beginning and end
-        Matcher m = sTrimPattern.matcher(s);
-        return m.replaceAll("$1");
+        return s.toString().replaceAll(TRIM_PATTERN, "").trim();
     }
 
     /**
@@ -601,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.
      *
@@ -630,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)
@@ -640,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) {
@@ -722,9 +744,54 @@
     }
 
     /**
-     * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent
+     * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CW. Parent
      * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine
      * the final bounds.
+     *
+     * As an example if this is the input:
+     * +-------------+
+     * |   +-----+   |
+     * |   |     |   |
+     * |   +-----+   |
+     * |             |
+     * |             |
+     * |             |
+     * +-------------+
+     * This would be case delta % 4 == 0:
+     * +-------------+
+     * |   +-----+   |
+     * |   |     |   |
+     * |   +-----+   |
+     * |             |
+     * |             |
+     * |             |
+     * +-------------+
+     * This would be case delta % 4 == 1:
+     * +----------------+
+     * |          +--+  |
+     * |          |  |  |
+     * |          |  |  |
+     * |          +--+  |
+     * |                |
+     * +----------------+
+     * This would be case delta % 4 == 2: // This is case was reverted to previous behaviour which
+     * doesn't match the illustration due to b/353965234
+     * +-------------+
+     * |             |
+     * |             |
+     * |             |
+     * |   +-----+   |
+     * |   |     |   |
+     * |   +-----+   |
+     * +-------------+
+     * This would be case delta % 4 == 3:
+     * +----------------+
+     * |  +--+          |
+     * |  |  |          |
+     * |  |  |          |
+     * |  +--+          |
+     * |                |
+     * +----------------+
      */
     public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight,
             int delta) {
@@ -830,6 +897,9 @@
             @NonNull Rect inclusionBounds,
             @NonNull Rect exclusionBounds,
             @AdjustmentDirection int adjustmentDirection) {
+        if (!Rect.intersects(targetViewBounds, exclusionBounds)) {
+            return;
+        }
         switch (adjustmentDirection) {
             case TRANSLATE_RIGHT:
                 targetView.setTranslationX(Math.min(
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 2995e8a..0e9c861 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -235,6 +235,7 @@
     boolean mChildrenLayersEnabled = true;
 
     private boolean mStripScreensOnPageStopMoving = false;
+    public boolean mHasOnLayoutBeenCalled = false;
 
     private boolean mWorkspaceFadeInAdjacentScreens;
 
@@ -1122,6 +1123,11 @@
         return super.onTouchEvent(ev);
     }
 
+    @Override
+    protected void onDisallowSwipeToMinusOnePage() {
+        mLauncher.getOverlayManager().onDisallowSwipeToMinusOnePage();
+    }
+
     /**
      * Called directly from a CellLayout (not by the framework), after we've been added as a
      * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout
@@ -1445,6 +1451,7 @@
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        mHasOnLayoutBeenCalled = true; // b/349929393 - is the required call to onLayout not done?
         if (mUnlockWallpaperFromDefaultPageOnLayout) {
             mWallpaperOffset.setLockToDefaultPage(false);
             mUnlockWallpaperFromDefaultPageOnLayout = false;
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
deleted file mode 100644
index 79b8187..0000000
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ /dev/null
@@ -1,89 +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.accessibility;
-
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.OnHierarchyChangeListener;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.dragndrop.DragController.DragListener;
-import com.android.launcher3.dragndrop.DragOptions;
-
-import java.util.function.Function;
-
-/**
- * Utility listener to enable/disable accessibility drag flags for a ViewGroup
- * containing CellLayouts
- */
-public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyChangeListener {
-
-    private final ViewGroup mViewGroup;
-    private final Function<CellLayout, DragAndDropAccessibilityDelegate> mDelegateFactory;
-
-    /**
-     * @param parent the viewgroup containing all the children
-     * @param delegateFactory function to create no delegates
-     */
-    public AccessibleDragListenerAdapter(ViewGroup parent,
-            Function<CellLayout, DragAndDropAccessibilityDelegate> delegateFactory) {
-        mViewGroup = parent;
-        mDelegateFactory = delegateFactory;
-    }
-
-    @Override
-    public void onDragStart(DragObject dragObject, DragOptions options) {
-        mViewGroup.setOnHierarchyChangeListener(this);
-        enableAccessibleDrag(true, dragObject);
-    }
-
-    @Override
-    public void onDragEnd() {
-        mViewGroup.setOnHierarchyChangeListener(null);
-        enableAccessibleDrag(false, null);
-        Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
-    }
-
-
-    @Override
-    public void onChildViewAdded(View parent, View child) {
-        if (parent == mViewGroup) {
-            setEnableForLayout((CellLayout) child, true);
-        }
-    }
-
-    @Override
-    public void onChildViewRemoved(View parent, View child) {
-        if (parent == mViewGroup) {
-            setEnableForLayout((CellLayout) child, false);
-        }
-    }
-
-    protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) {
-        for (int i = 0; i < mViewGroup.getChildCount(); i++) {
-            setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
-        }
-    }
-
-    protected final void setEnableForLayout(CellLayout layout, boolean enable) {
-        layout.setDragAndDropAccessibilityDelegate(enable ? mDelegateFactory.apply(layout) : null);
-    }
-}
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
new file mode 100644
index 0000000..21c2caf
--- /dev/null
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.accessibility
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.views.ActivityContext
+import java.util.function.Function
+
+/**
+ * Utility listener to enable/disable accessibility drag flags for a ViewGroup containing
+ * CellLayouts
+ */
+open class AccessibleDragListenerAdapter
+/**
+ * @param parent the viewgroup containing all the children
+ * @param delegateFactory function to create no delegates
+ */
+(
+    private val mViewGroup: ViewGroup,
+    private val mDelegateFactory: Function<CellLayout, DragAndDropAccessibilityDelegate>
+) : DragController.DragListener, ViewGroup.OnHierarchyChangeListener {
+    override fun onDragStart(dragObject: DragObject, options: DragOptions) {
+        mViewGroup.setOnHierarchyChangeListener(this)
+        enableAccessibleDrag(true, dragObject)
+    }
+
+    override fun onDragEnd() {
+        mViewGroup.setOnHierarchyChangeListener(null)
+        enableAccessibleDrag(false, null)
+        val activityContext = ActivityContext.lookupContext(mViewGroup.context) as ActivityContext
+        activityContext.getDragController<DragController<*>>()?.removeDragListener(this)
+    }
+
+    override fun onChildViewAdded(parent: View, child: View) {
+        if (parent === mViewGroup) {
+            setEnableForLayout(child as CellLayout, true)
+        }
+    }
+
+    override fun onChildViewRemoved(parent: View, child: View) {
+        if (parent === mViewGroup) {
+            setEnableForLayout(child as CellLayout, false)
+        }
+    }
+
+    protected open fun enableAccessibleDrag(enable: Boolean, dragObject: DragObject?) {
+        for (i in 0 until mViewGroup.childCount) {
+            setEnableForLayout(mViewGroup.getChildAt(i) as CellLayout, enable)
+        }
+    }
+
+    protected fun setEnableForLayout(layout: CellLayout, enable: Boolean) {
+        layout.setDragAndDropAccessibilityDelegate(
+            if (enable) mDelegateFactory.apply(layout) else null
+        )
+    }
+}
diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
index d0fc175..6f73e07 100644
--- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
@@ -29,9 +29,9 @@
 import androidx.customview.widget.ExploreByTouchHelper;
 
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
 
 import java.util.List;
 
@@ -47,16 +47,17 @@
 
     protected final CellLayout mView;
     protected final Context mContext;
+    protected final ActivityContext mActivityContext;
     protected final LauncherAccessibilityDelegate mDelegate;
-    protected final DragLayer mDragLayer;
+    protected final BaseDragLayer<?> mDragLayer;
 
     public DragAndDropAccessibilityDelegate(CellLayout forView) {
         super(forView);
         mView = forView;
         mContext = mView.getContext();
-        Launcher launcher = Launcher.getLauncher(mContext);
-        mDelegate = launcher.getAccessibilityDelegate();
-        mDragLayer = launcher.getDragLayer();
+        mActivityContext = ActivityContext.lookupContext(mContext);
+        mDelegate = (LauncherAccessibilityDelegate) mActivityContext.getAccessibilityDelegate();
+        mDragLayer = mActivityContext.getDragLayer();
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 56a7fef..1b58987 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -18,16 +18,16 @@
 import static com.android.launcher3.Flags.enableExpandingPauseWorkButton;
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
+import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.WORK;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
-import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
 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 android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -47,7 +47,6 @@
 import android.os.Process;
 import android.os.UserManager;
 import android.util.AttributeSet;
-import android.util.FloatProperty;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.KeyEvent;
@@ -64,6 +63,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
 import androidx.annotation.VisibleForTesting;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.core.graphics.ColorUtils;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -71,6 +71,7 @@
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.R;
@@ -94,6 +95,7 @@
 import com.android.launcher3.views.ScrimView;
 import com.android.launcher3.views.SpringRelativeLayout;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip;
+import com.android.systemui.plugins.AllAppsRow;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -113,19 +115,6 @@
         ScrimView.ScrimDrawingController {
 
 
-    public static final FloatProperty<ActivityAllAppsContainerView<?>> BOTTOM_SHEET_ALPHA =
-            new FloatProperty<>("bottomSheetAlpha") {
-                @Override
-                public Float get(ActivityAllAppsContainerView<?> containerView) {
-                    return containerView.mBottomSheetAlpha;
-                }
-
-                @Override
-                public void setValue(ActivityAllAppsContainerView<?> containerView, float v) {
-                    containerView.setBottomSheetAlpha(v);
-                }
-            };
-
     public static final float PULL_MULTIPLIER = .02f;
     public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
     protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
@@ -164,8 +153,10 @@
     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;
 
     /**
      * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}.
@@ -188,8 +179,6 @@
     private ScrimView mScrimView;
     private int mHeaderColor;
     private int mBottomSheetBackgroundColor;
-    private float mBottomSheetAlpha = 1f;
-    private boolean mForceBottomSheetVisible;
     private int mTabsProtectionAlpha;
     @Nullable private AllAppsTransitionController mAllAppsTransitionController;
 
@@ -275,21 +264,38 @@
 
         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);
         mFastScroller = findViewById(R.id.fast_scroller);
         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
+        mFastScrollLetterLayout = findViewById(R.id.scroll_letter_layout);
+        if (Flags.letterFastScroller()) {
+            // Set clip children to false otherwise the scroller letters will be clipped.
+            setClipChildren(false);
+        } else {
+            setClipChildren(true);
+        }
 
         mSearchContainer = inflateSearchBar();
         if (!isSearchBarFloating()) {
             // 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();
@@ -337,20 +343,6 @@
         return mSearchUiManager;
     }
 
-    public View getBottomSheetBackground() {
-        return mBottomSheetBackground;
-    }
-
-    /**
-     * Temporarily force the bottom sheet to be visible on non-tablets.
-     *
-     * @param force {@code true} means bottom sheet will be visible on phones until {@code reset()}.
-     */
-    public void forceBottomSheetVisible(boolean force) {
-        mForceBottomSheetVisible = force;
-        updateBackgroundVisibility(mActivityContext.getDeviceProfile());
-    }
-
     public View getSearchView() {
         return mSearchContainer;
     }
@@ -482,7 +474,7 @@
         if (mHeader != null && mHeader.getVisibility() == VISIBLE) {
             mHeader.reset(animate);
         }
-        forceBottomSheetVisible(false);
+        updateBackgroundVisibility(mActivityContext.getDeviceProfile());
         // Reset the base recycler view after transitioning home.
         updateHeaderScroll(0);
         if (exitSearch) {
@@ -561,7 +553,8 @@
             mActivityContext.hideKeyboard();
         }
         if (mAH.get(currentActivePage).mRecyclerView != null) {
-            mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller);
+            mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller,
+                    ALL_APPS_SCROLLER);
         }
         // Header keeps track of active recycler view to properly render header protection.
         mHeader.setActiveRV(currentActivePage);
@@ -666,18 +659,13 @@
             @NonNull AllAppsRecyclerView mainRecyclerView,
             @Nullable AllAppsRecyclerView workRecyclerView,
             @NonNull AllAppsRecyclerViewPool recycledViewPool) {
-        if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
-            return;
-        }
         final boolean hasWorkProfile = workRecyclerView != null;
         recycledViewPool.setHasWorkProfile(hasWorkProfile);
         mainRecyclerView.setRecycledViewPool(recycledViewPool);
         if (workRecyclerView != null) {
             workRecyclerView.setRecycledViewPool(recycledViewPool);
         }
-        if (ALL_APPS_GONE_VISIBILITY.get()) {
-            mainRecyclerView.updatePoolSize(hasWorkProfile);
-        }
+        mainRecyclerView.updatePoolSize(hasWorkProfile);
     }
 
     private void replaceAppsRVContainer(boolean showTabs) {
@@ -716,15 +704,13 @@
             post(() -> mAH.get(AdapterHolder.WORK).applyPadding());
 
         } else {
-            mWorkManager.detachWorkModeSwitch();
+            mWorkManager.detachWorkUtilityViews();
             mViewPager = null;
         }
 
         removeCustomRules(rvContainer);
         removeCustomRules(getSearchRecyclerView());
-        if (!isSearchSupported()) {
-            layoutWithoutSearchContainer(rvContainer, showTabs);
-        } else if (isSearchBarFloating()) {
+        if (isSearchBarFloating()) {
             alignParentTop(rvContainer, showTabs);
             alignParentTop(getSearchRecyclerView(), /* tabs= */ false);
         } else {
@@ -736,6 +722,8 @@
     }
 
     void setupHeader() {
+        mAdditionalHeaderRows.forEach(row -> mHeader.onPluginDisconnected(row));
+
         mHeader.setVisibility(View.VISIBLE);
         boolean tabsHidden = !mUsingTabs;
         mHeader.setup(
@@ -753,17 +741,26 @@
                 adapterHolder.mRecyclerView.scrollToTop();
             }
         });
+        mAdditionalHeaderRows.forEach(row -> mHeader.onPluginConnected(row, mActivityContext));
 
         removeCustomRules(mHeader);
-        if (!isSearchSupported()) {
-            layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */);
-        } else if (isSearchBarFloating()) {
+        if (isSearchBarFloating()) {
             alignParentTop(mHeader, false /* includeTabsMargin */);
         } else {
             layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */);
         }
     }
 
+    /**
+     * Force header height update with an offset. Used by {@link UniversalSearchInputView} to
+     * request {@link FloatingHeaderView} to update its maxTranslation for multiline search bar.
+     */
+    public void forceUpdateHeaderHeight(int offset) {
+        if (Flags.multilineSearchBar()) {
+            mHeader.updateSearchBarOffset(offset);
+        }
+    }
+
     protected void updateHeaderScroll(int scrolledOffset) {
         float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f);
         int headerColor = getHeaderColor(prog1);
@@ -902,23 +899,6 @@
                 mMainAdapterProvider);
     }
 
-    // TODO(b/216683257): Remove when Taskbar All Apps supports search.
-    protected boolean isSearchSupported() {
-        return true;
-    }
-
-    private void layoutWithoutSearchContainer(View v, boolean includeTabsMargin) {
-        if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
-            return;
-        }
-
-        RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
-        layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
-        layoutParams.topMargin = getContext().getResources().getDimensionPixelSize(includeTabsMargin
-                ? R.dimen.all_apps_header_pill_height
-                : R.dimen.all_apps_header_top_margin);
-    }
-
     public boolean isInAllApps() {
         // TODO: Make this abstract
         return true;
@@ -1003,18 +983,13 @@
     }
 
     protected void updateBackgroundVisibility(DeviceProfile deviceProfile) {
-        boolean visible = deviceProfile.isTablet || mForceBottomSheetVisible;
-        mBottomSheetBackground.setVisibility(visible ? View.VISIBLE : View.GONE);
-        // Note: For tablets, the opaque background and header protection are added in drawOnScrim.
+        mBottomSheetBackground.setVisibility(
+                deviceProfile.shouldShowAllAppsOnSheet() ? View.VISIBLE : View.GONE);
+        // Note: The opaque sheet background and header protection are added in drawOnScrim.
         // For the taskbar entrypoint, the scrim is drawn by its abstract slide in view container,
         // so its header protection is derived from this scrim instead.
     }
 
-    private void setBottomSheetAlpha(float alpha) {
-        // Bottom sheet alpha is always 1 for tablets.
-        mBottomSheetAlpha = mActivityContext.getDeviceProfile().isTablet ? 1f : alpha;
-    }
-
     @VisibleForTesting
     public void onAppsUpdated() {
         mHasWorkApps = Stream.of(mAllAppsStore.getApps())
@@ -1152,8 +1127,8 @@
         applyAdapterSideAndBottomPaddings(grid);
 
         MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
-        // Ignore left/right insets on tablet because we are already centered in-screen.
-        if (grid.isTablet) {
+        // Ignore left/right insets on bottom sheet because we are already centered in-screen.
+        if (grid.shouldShowAllAppsOnSheet()) {
             mlp.leftMargin = mlp.rightMargin = 0;
         } else {
             mlp.leftMargin = insets.left;
@@ -1258,8 +1233,8 @@
     /** Called in Launcher#bindStringCache() to update the UI when cache is updated. */
     public void updateWorkUI() {
         setDeviceManagementResources();
-        if (mWorkManager.getWorkModeSwitch() != null) {
-            mWorkManager.getWorkModeSwitch().updateStringFromCache();
+        if (mWorkManager.getWorkUtilityView() != null) {
+            mWorkManager.getWorkUtilityView().updateStringFromCache();
         }
         inflateWorkCardsIfNeeded();
     }
@@ -1302,6 +1277,10 @@
         return mAH.get(MAIN).mAppsList;
     }
 
+    public AlphabeticalAppsList<T> getWorkAppList() {
+        return mAH.get(WORK).mAppsList;
+    }
+
     public FloatingHeaderView getFloatingHeaderView() {
         return mHeader;
     }
@@ -1395,7 +1374,7 @@
         // Draw full background panel for tablets.
         if (hasBottomSheet) {
             mHeaderPaint.setColor(mBottomSheetBackgroundColor);
-            mHeaderPaint.setAlpha((int) (255 * mBottomSheetAlpha));
+            mHeaderPaint.setAlpha(255);
 
             mTmpRectF.set(
                     leftWithScale,
@@ -1484,6 +1463,10 @@
         }
     }
 
+    ConstraintLayout getFastScrollerLetterList() {
+        return mFastScrollLetterLayout;
+    }
+
     /**
      * redraws header protection
      */
@@ -1551,7 +1534,7 @@
         void setup(@NonNull View rv, @Nullable Predicate<ItemInfo> matcher) {
             mAppsList.updateItemFilter(matcher);
             mRecyclerView = (AllAppsRecyclerView) rv;
-            mRecyclerView.bindFastScrollbar(mFastScroller);
+            mRecyclerView.bindFastScrollbar(mFastScroller, ALL_APPS_SCROLLER);
             mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory());
             mRecyclerView.setApps(mAppsList);
             mRecyclerView.setLayoutManager(mLayoutManager);
@@ -1574,8 +1557,8 @@
         void applyPadding() {
             if (mRecyclerView != null) {
                 int bottomOffset = 0;
-                if (isWork() && mWorkManager.getWorkModeSwitch() != null) {
-                    bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight();
+                if (isWork() && mWorkManager.getWorkUtilityView() != null) {
+                    bottomOffset = mInsets.bottom + mWorkManager.getWorkUtilityView().getHeight();
                 } else if (isMain() && mPrivateProfileManager != null) {
                     Optional<AdapterItem> privateSpaceHeaderItem = mAppsList.getAdapterItems()
                             .stream()
diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
index 911612f..77a0fe3 100644
--- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
+++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps;
 
+import static android.view.HapticFeedbackConstants.CLOCK_TICK;
+
 import androidx.recyclerview.widget.LinearSmoothScroller;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
@@ -71,6 +73,7 @@
 
         @Override
         protected int getVerticalSnapPreference() {
+            mRv.performHapticFeedback(CLOCK_TICK);
             return SNAP_TO_ANY;
         }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 2a47222..51d1c9f 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,8 +15,9 @@
  */
 package com.android.launcher3.allapps;
 
-import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
+import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT;
+import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
+
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN;
@@ -36,22 +37,29 @@
 import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
 import androidx.core.util.Consumer;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.FastScrollRecyclerView;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.views.ActivityContext;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -66,6 +74,7 @@
     protected final int mNumAppsPerRow;
     private final AllAppsFastScrollHelper mFastScrollHelper;
     private int mCumulativeVerticalScroll;
+    private ConstraintLayout mLetterList;
 
     protected AlphabeticalAppsList<?> mApps;
 
@@ -113,13 +122,11 @@
         // all apps.
         int maxPoolSizeForAppIcons = grid.getMaxAllAppsRowCount()
                 * grid.numShownAllAppsColumns;
-        if (ALL_APPS_GONE_VISIBILITY.get() && ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
-            // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to
-            // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows +
-            // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly.
-            maxPoolSizeForAppIcons +=
-                    PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT;
-        }
+        // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to
+        // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows +
+        // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly.
+        maxPoolSizeForAppIcons +=
+                PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT;
         if (hasWorkProfile) {
             maxPoolSizeForAppIcons *= 2;
         }
@@ -238,6 +245,9 @@
             return;
         }
 
+        if (Flags.letterFastScroller() && !mScrollbar.isDraggingThumb()) {
+            setLettersToScrollLayout(mApps.getFastScrollerSections());
+        }
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
         int availableScrollHeight = getAvailableScrollHeight();
@@ -319,6 +329,80 @@
         return false;
     }
 
+    public void setLettersToScrollLayout(
+            List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections) {
+        if (fastScrollSections.isEmpty()) {
+            return;
+        }
+        if (mLetterList != null) {
+            mLetterList.removeAllViews();
+        }
+        Context context = getContext();
+        ActivityAllAppsContainerView<?> allAppsContainerView =
+                ActivityContext.lookupContext(context).getAppsView();
+        mLetterList = allAppsContainerView.getFastScrollerLetterList();
+        mLetterList.setPadding(0, getScrollBarTop(), 0, getScrollBarMarginBottom());
+        List<LetterListTextView> textViews = new ArrayList<>();
+        for (int i = 0; i < fastScrollSections.size(); i++) {
+            AlphabeticalAppsList.FastScrollSectionInfo sectionInfo = fastScrollSections.get(i);
+            LetterListTextView textView =
+                    (LetterListTextView) LayoutInflater.from(context).inflate(
+                            R.layout.fast_scroller_letter_list_text_view, mLetterList, false);
+            int viewId = View.generateViewId();
+            textView.apply(sectionInfo /* FastScrollSectionInfo */, viewId /* viewId */);
+            sectionInfo.setId(viewId);
+            if (i == fastScrollSections.size() - 1) {
+                // The last section info is just a duplicate so that user can scroll to the bottom.
+                textView.setVisibility(INVISIBLE);
+            }
+            textViews.add(textView);
+            mLetterList.addView(textView);
+        }
+        // Need to add an extra textview to be aligned.
+        LetterListTextView lastLetterListTextView = new LetterListTextView(context);
+        int currentId = View.generateViewId();
+        lastLetterListTextView.setId(currentId);
+        lastLetterListTextView.setVisibility(INVISIBLE);
+        textViews.add(lastLetterListTextView);
+        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,
+            List<LetterListTextView> textViews) {
+        ConstraintSet chain = new ConstraintSet();
+        chain.clone(constraintLayout);
+        for (int i = 0; i < textViews.size(); i++) {
+            LetterListTextView currentView = textViews.get(i);
+            if (i == 0) {
+                chain.connect(currentView.getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID,
+                        ConstraintSet.TOP);
+            } else {
+                chain.connect(currentView.getId(), ConstraintSet.TOP, textViews.get(i-1).getId(),
+                        ConstraintSet.BOTTOM);
+            }
+            chain.connect(currentView.getId(), ConstraintSet.START, constraintLayout.getId(),
+                    ConstraintSet.START);
+            chain.connect(currentView.getId(), ConstraintSet.END, constraintLayout.getId(),
+                    ConstraintSet.END);
+        }
+        int[] viewIds = textViews.stream().mapToInt(TextView::getId).toArray();
+        float[] weights = new float[textViews.size()];
+        Arrays.fill(weights,1); // fill with 1 for equal weights
+        chain.createVerticalChain(constraintLayout.getId(), ConstraintSet.TOP,
+                constraintLayout.getId(), ConstraintSet.BOTTOM, viewIds, weights,
+                ConstraintSet.CHAIN_SPREAD);
+        chain.applyTo(constraintLayout);
+    }
+
+    @Override
+    public ConstraintLayout getLetterList() {
+        return mLetterList;
+    }
+
     private void logCumulativeVerticalScroll() {
         ActivityContext context = ActivityContext.lookupContext(getContext());
         StatsLogManager mgr = context.getStatsLogManager();
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 89e6adc..29b9e77 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.allapps;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
 import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
@@ -109,7 +108,7 @@
         mPackageUserKeytoUidMap = map;
         // Preinflate all apps RV when apps has changed, which can happen after unlocking screen,
         // rotating screen, or downloading/upgrading apps.
-        if (shouldPreinflate && ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+        if (shouldPreinflate) {
             mAllAppsRecyclerViewPool.preInflateAllAppsViewHolders(mContext);
         }
     }
@@ -262,11 +261,12 @@
         writer.println(prefix + "\tAllAppsStore Apps[] size: " + mApps.length);
         for (int i = 0; i < mApps.length; i++) {
             writer.println(String.format(Locale.getDefault(),
-                    "%s\tPackage index, name, and class: " + "%d/%s:%s",
+                    "%s\tPackage index, name, class, and description: %d/%s:%s, %s",
                     prefix,
                     i,
                     mApps[i].componentName.getPackageName(),
-                    mApps[i].componentName.getClassName()));
+                    mApps[i].componentName.getClassName(),
+                    mApps[i].contentDescription));
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 1b0ad04..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;
@@ -37,7 +35,6 @@
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
 import android.util.FloatProperty;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
@@ -46,28 +43,28 @@
 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;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.config.FeatureFlags;
 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
@@ -109,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();
@@ -118,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);
@@ -137,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();
@@ -146,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);
@@ -171,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
@@ -186,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() {
@@ -220,7 +216,7 @@
             mLauncher.getWorkspace().getPageIndicator().setTranslationY(0);
         }
 
-        mIsTablet = dp.isTablet;
+        mShouldShowAllAppsOnSheet = dp.shouldShowAllAppsOnSheet();
     }
 
     /**
@@ -283,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);
     }
@@ -359,22 +354,6 @@
             });
         }
 
-        if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.isUserControlled()
-                && Utilities.ATLEAST_S) {
-            if (toState == ALL_APPS) {
-                builder.addOnFrameListener(
-                        new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
-                                SWIPE_DRAG_COMMIT_THRESHOLD, 1));
-            } else {
-                builder.addOnFrameListener(
-                        new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
-                                0, SWIPE_DRAG_COMMIT_THRESHOLD));
-            }
-            builder.addEndListener((unused) -> {
-                mVibratorWrapper.cancelVibrate();
-            });
-        }
-
         float targetProgress = toState.getVerticalProgress(mLauncher);
         if (Float.compare(mProgress, targetProgress) == 0) {
             setAlphas(toState, config, builder);
@@ -391,10 +370,17 @@
 
         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) &&
-                !FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get()) {
-            mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+        if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
+            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);
+            }
         }
     }
 
@@ -415,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);
@@ -432,8 +414,7 @@
         mAppsView = appsView;
         mAppsView.setScrimView(scrimView);
 
-        mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT,
-                FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE);
+        mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT, View.GONE);
         mAppsViewAlpha.setUpdateVisibility(true);
         mAppsViewTranslationY = new MultiPropertyFactory<>(
                 mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum);
@@ -445,45 +426,4 @@
     public void setShiftRange(float shiftRange) {
         mShiftRange = shiftRange;
     }
-
-    /**
-     * This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start
-     * threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller
-     * as it gives an accurate progress that dictates where the vibrator should vibrate.
-     * Note: once the user begins a gesture and does the commit haptic, there should not be anymore
-     * haptics played for that gesture.
-     */
-    private static class VibrationAnimatorUpdateListener implements
-            ValueAnimator.AnimatorUpdateListener {
-        private final VibratorWrapper mVibratorWrapper;
-        private final AllAppsTransitionController mController;
-        private final float mStartThreshold;
-        private final float mEndThreshold;
-        private boolean mHasCommitted;
-
-        VibrationAnimatorUpdateListener(AllAppsTransitionController controller,
-                                        VibratorWrapper vibratorWrapper, float startThreshold,
-                                        float endThreshold) {
-            mController = controller;
-            mVibratorWrapper = vibratorWrapper;
-            mStartThreshold = startThreshold;
-            mEndThreshold = endThreshold;
-        }
-
-        @Override
-        public void onAnimationUpdate(ValueAnimator animation) {
-            if (mHasCommitted) {
-                return;
-            }
-            float currentProgress =
-                    AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController);
-            if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) {
-                mVibratorWrapper.vibrateForDragTexture();
-            } else if (!(currentProgress == 0 || currentProgress == 1)) {
-                // This check guards against committing at the location of the start of the gesture
-                mVibratorWrapper.vibrateForDragCommit();
-                mHasCommitted = true;
-            }
-        }
-    }
 }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 5d03a93..709b52a 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
@@ -25,6 +26,7 @@
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.style.ImageSpan;
+import android.util.Log;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -38,6 +40,7 @@
 import com.android.launcher3.util.LabelComparator;
 import com.android.launcher3.views.ActivityContext;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -71,11 +74,17 @@
         public final CharSequence sectionName;
         // The item position
         public final int position;
+        // The view id associated with this section
+        public int id = -1;
 
         public FastScrollSectionInfo(CharSequence sectionName, int position) {
             this.sectionName = sectionName;
             this.position = position;
         }
+
+        public void setId(int id) {
+            this.id = id;
+        }
     }
 
 
@@ -97,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;
@@ -115,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. */
@@ -269,6 +284,7 @@
         List<AdapterItem> oldItems = new ArrayList<>(mAdapterItems);
         // Prepare to update the list of sections, filtered apps, etc.
         mFastScrollerSections.clear();
+        Log.d(TAG, "Clearing FastScrollerSections.");
         mAdapterItems.clear();
         mAccessibilityResultsCount = 0;
 
@@ -289,12 +305,22 @@
                     mFastScrollerSections.add(new FastScrollSectionInfo(
                             mActivityContext.getResources().getString(
                                     R.string.work_profile_edu_section), 0));
+                    Log.d(TAG, "Adding FastScrollSection for work edu card.");
                 }
                 position = addAppsWithSections(mApps, position);
             }
             if (Flags.enablePrivateSpace()) {
                 position = addPrivateSpaceItems(position);
             }
+            if (!mFastScrollerSections.isEmpty()) {
+                // After all the adapterItems are added, add a view to the bottom so that user can
+                // scroll all the way down.
+                mAdapterItems.add(new AdapterItem(VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO));
+                mFastScrollerSections.add(new FastScrollSectionInfo(
+                        mFastScrollerSections.get(mFastScrollerSections.size() - 1).sectionName,
+                        position++));
+                Log.d(TAG, "Adding FastScrollSection duplicate to scroll to the bottom.");
+            }
         }
         mAccessibilityResultsCount = (int) mAdapterItems.stream()
                 .filter(AdapterItem::isCountedForAccessibility).count();
@@ -337,6 +363,7 @@
                 && !mPrivateApps.isEmpty()) {
             // Always add PS Header if Space is present and visible.
             position = mPrivateProviderManager.addPrivateSpaceHeader(mAdapterItems);
+            Log.d(TAG, "Adding FastScrollSection for Private Space header. ");
             mFastScrollerSections.add(new FastScrollSectionInfo(
                     mPrivateProfileAppScrollerBadge, position));
             int privateSpaceState = mPrivateProviderManager.getCurrentState();
@@ -383,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);
@@ -398,14 +430,14 @@
             hasPrivateApps = appList.stream().
                     allMatch(mPrivateProviderManager.getItemInfoMatcher());
         }
+        Log.d(TAG, "Adding apps with sections. HasPrivateApps: " + hasPrivateApps);
         for (int i = 0; i < appList.size(); i++) {
             AppInfo info = appList.get(i);
             // Apply decorator to private apps.
             if (hasPrivateApps) {
                 mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info,
-                        new SectionDecorationInfo(mActivityContext.getApplicationContext(),
-                                getRoundRegions(i, appList.size()),
-                                true /* decorateTogether */)));
+                        new SectionDecorationInfo(mActivityContext,
+                                getRoundRegions(i, appList.size()), true /* decorateTogether */)));
             } else {
                 mAdapterItems.add(AdapterItem.asApp(info));
             }
@@ -413,9 +445,14 @@
             String sectionName = info.sectionName;
             // Create a new section if the section names do not match
             if (!sectionName.equals(lastSectionName)) {
+                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++;
         }
@@ -463,6 +500,13 @@
         return mPrivateProviderManager;
     }
 
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "SectionInfo[] size: " + mFastScrollerSections.size());
+        for (int i = 0; i < mFastScrollerSections.size(); i++) {
+            writer.println("\tFastScrollSection: " + mFastScrollerSections.get(i).sectionName);
+        }
+    }
+
     private static class MyDiffCallback extends DiffUtil.Callback {
 
         private final List<AdapterItem> mOldList;
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 4b38df8..60bf3ea 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -68,7 +68,8 @@
     public static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 5;
     public static final int VIEW_TYPE_PRIVATE_SPACE_HEADER = 1 << 6;
     public static final int VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER = 1 << 7;
-    public static final int NEXT_ID = 8;
+    public static final int VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO = 1 << 8;
+    public static final int NEXT_ID = 9;
 
     // Common view type masks
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
@@ -247,6 +248,8 @@
             case VIEW_TYPE_PRIVATE_SPACE_HEADER:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.private_space_header, parent, false));
+            case VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO:
+                return new ViewHolder(new View(mActivityContext));
             default:
                 if (mAdapterProvider.isViewSupported(viewType)) {
                     return mAdapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType);
@@ -324,6 +327,7 @@
                         == STATE_DISABLED ? null : new SectionDecorationInfo(mActivityContext,
                         ROUND_NOTHING, true /* decorateTogether */);
                 break;
+            case VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO:
             case VIEW_TYPE_ALL_APPS_DIVIDER:
             case VIEW_TYPE_WORK_DISABLED_CARD:
                 // nothing to do
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 92c589c..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;
@@ -30,10 +32,10 @@
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.PluginManagerWrapper;
 import com.android.launcher3.views.ActivityContext;
 import com.android.systemui.plugins.AllAppsRow;
@@ -104,14 +106,16 @@
     private boolean mFloatingRowsCollapsed;
     // Total height of all current floating rows. Collapsed rows == 0 height.
     private int mFloatingRowsHeight;
+    // Offset of search bar. Adds to the floating view height when multi-line is supported.
+    private int mSearchBarOffset = 0;
 
     // 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);
@@ -178,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);
@@ -198,9 +206,20 @@
         }
     }
 
+    /**
+     * Offset floating rows height by search bar
+     */
+    void updateSearchBarOffset(int offset) {
+        mSearchBarOffset = offset;
+        onHeightUpdated();
+    }
+
     @Override
     public void onPluginDisconnected(AllAppsRow plugin) {
         PluginHeaderRow row = mPluginRows.get(plugin);
+        if (row == null) {
+            return;
+        }
         removeView(row.mView);
         mPluginRows.remove(plugin);
         recreateAllRowsArray();
@@ -209,15 +228,12 @@
 
     @Override
     public View getFocusedChild() {
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-            for (FloatingHeaderRow row : mAllRows) {
-                if (row.hasVisibleContent() && row.isVisible()) {
-                    return row.getFocusedChild();
-                }
+        for (FloatingHeaderRow row : mAllRows) {
+            if (row.hasVisibleContent() && row.isVisible()) {
+                return row.getFocusedChild();
             }
-            return null;
         }
-        return super.getFocusedChild();
+        return null;
     }
 
     void setup(AllAppsRecyclerView mainRV, AllAppsRecyclerView workRV, SearchRecyclerView searchRV,
@@ -258,9 +274,18 @@
         mTabLayout.setVisibility(mTabsHidden ? GONE : visibility);
     }
 
+    /** Returns whether search bar has multi-line support, and is currently in multi-line state. */
+    private boolean isSearchBarMultiline() {
+        return Flags.multilineSearchBar() && mSearchBarOffset > 0;
+    }
+
     private void updateExpectedHeight() {
         updateFloatingRowsHeight();
         mMaxTranslation = 0;
+        boolean shouldAddSearchBarHeight = isSearchBarMultiline() && !Flags.floatingSearchBar();
+        if (shouldAddSearchBarHeight) {
+            mMaxTranslation += mSearchBarOffset;
+        }
         if (mFloatingRowsCollapsed) {
             return;
         }
diff --git a/src/com/android/launcher3/allapps/FloatingMaskView.java b/src/com/android/launcher3/allapps/FloatingMaskView.java
index 606eb03..cee5e18 100644
--- a/src/com/android/launcher3/allapps/FloatingMaskView.java
+++ b/src/com/android/launcher3/allapps/FloatingMaskView.java
@@ -21,6 +21,7 @@
 import android.view.ViewGroup;
 import android.widget.ImageView;
 
+import androidx.annotation.VisibleForTesting;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
@@ -53,13 +54,21 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
-        AllAppsRecyclerView allAppsContainerView =
-                mActivityContext.getAppsView().getActiveRecyclerView();
+        setParameters((ViewGroup.MarginLayoutParams) getLayoutParams(),
+                mActivityContext.getAppsView().getActiveRecyclerView());
+    }
+
+    @VisibleForTesting
+    void setParameters(ViewGroup.MarginLayoutParams lp, AllAppsRecyclerView recyclerView) {
         if (lp != null) {
-            lp.rightMargin = allAppsContainerView.getPaddingRight();
-            lp.leftMargin = allAppsContainerView.getPaddingLeft();
-            mBottomBox.setMinimumHeight(allAppsContainerView.getPaddingBottom());
+            lp.rightMargin = recyclerView.getPaddingRight();
+            lp.leftMargin = recyclerView.getPaddingLeft();
+            getBottomBox().setMinimumHeight(recyclerView.getPaddingBottom());
         }
     }
+
+    @VisibleForTesting
+    ImageView getBottomBox() {
+        return mBottomBox;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/LetterListTextView.java b/src/com/android/launcher3/allapps/LetterListTextView.java
new file mode 100644
index 0000000..8586078
--- /dev/null
+++ b/src/com/android/launcher3/allapps/LetterListTextView.java
@@ -0,0 +1,132 @@
+/*
+ * 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.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;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Themes;
+
+/**
+ * A TextView that is used to display the letter list in the fast scroller.
+ */
+public class LetterListTextView extends TextView {
+    private static final float ABSOLUTE_TRANSLATION_X = 30f;
+    private static final float ABSOLUTE_SCALE = 1.4f;
+    private final Drawable mLetterBackground;
+    private final int mLetterListTextWidthAndHeight;
+    private final int mTextColor;
+
+    public LetterListTextView(Context context) {
+        this(context, null, 0);
+    }
+
+    public LetterListTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public LetterListTextView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mLetterBackground = context.getDrawable(R.drawable.bg_letter_list_text);
+        mLetterListTextWidthAndHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.fastscroll_list_letter_size);
+        mTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurface);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+        setBackground(mLetterBackground);
+        setTextColor(mTextColor);
+        setClickable(false);
+        setWidth(mLetterListTextWidthAndHeight);
+        setTextSize(mLetterListTextWidthAndHeight);
+        setVisibility(VISIBLE);
+    }
+
+    /**
+     * 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
+     *                       pixels.
+     */
+    public void animateBasedOnYPosition(int currentFingerY) {
+        if (getBackground() == null) {
+            return;
+        }
+        float cutOffMin = currentFingerY - (getHeight() * 2);
+        float cutOffMax = currentFingerY + (getHeight() * 2);
+        float cutOffDistance = cutOffMax - cutOffMin;
+        boolean isWithinAnimationBounds = getY() < cutOffMax && getY() > cutOffMin;
+        translateBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds);
+        scaleBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds);
+    }
+
+    private void scaleBasedOnYPosition(int y, float cutOffDistance,
+            boolean isWithinAnimationBounds) {
+        float raisedCosineScale = (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI)
+                * ABSOLUTE_SCALE;
+        if (isWithinAnimationBounds) {
+            raisedCosineScale = Utilities.boundToRange(raisedCosineScale, 1f, ABSOLUTE_SCALE);
+            setScaleX(raisedCosineScale);
+            setScaleY(raisedCosineScale);
+        } else {
+            setScaleX(1);
+            setScaleY(1);
+        }
+    }
+
+    private void translateBasedOnYPosition(int y, float cutOffDistance,
+            boolean isWithinAnimationBounds) {
+        float raisedCosineTranslation =
+                (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI)
+                        * ABSOLUTE_TRANSLATION_X;
+        if (isWithinAnimationBounds) {
+            raisedCosineTranslation = -1 * Utilities.boundToRange(raisedCosineTranslation,
+                    0, ABSOLUTE_TRANSLATION_X);
+            setTranslationX(raisedCosineTranslation);
+        } else {
+            setTranslationX(0);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 0f4204f..21dce14 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -41,7 +41,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
@@ -67,6 +66,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;
@@ -92,14 +92,14 @@
 public class PrivateProfileManager extends UserProfileManager {
 
     private static final String TAG = "PrivateProfileManager";
-    private static final int EXPAND_COLLAPSE_DURATION = 800;
+    private static final int EXPAND_COLLAPSE_DURATION = 400;
     private static final int SETTINGS_OPACITY_DURATION = 400;
     private static final int TEXT_UNLOCK_OPACITY_DURATION = 300;
     private static final int TEXT_LOCK_OPACITY_DURATION = 50;
     private static final int APP_OPACITY_DURATION = 400;
     private static final int MASK_VIEW_DURATION = 200;
     private static final int APP_OPACITY_DELAY = 400;
-    private static final int SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY = 400;
+    private static final int PILL_TRANSITION_DELAY = 400;
     private static final int SETTINGS_OPACITY_DELAY = 400;
     private static final int LOCK_TEXT_OPACITY_DELAY = 500;
     private static final int MASK_VIEW_DELAY = 400;
@@ -109,6 +109,8 @@
     private final Predicate<UserHandle> mPrivateProfileMatcher;
     private final int mPsHeaderHeight;
     private final int mFloatingMaskViewCornerRadius;
+    private final int mLockTextMarginStart;
+    private final int mLockTextMarginEnd;
     private final RecyclerView.OnScrollListener mOnIdleScrollListener =
             new RecyclerView.OnScrollListener() {
         @Override
@@ -133,6 +135,11 @@
     private Runnable mOnPSHeaderAdded;
     @Nullable
     private RelativeLayout mPSHeader;
+    @Nullable
+    private TextView mLockText;
+    @Nullable
+    private PrivateSpaceSettingsButton mPrivateSpaceSettingsButton;
+    @Nullable
     private ConstraintLayout mFloatingMaskView;
     private final String mLockedStateContentDesc;
     private final String mUnLockedStateContentDesc;
@@ -155,6 +162,10 @@
                 .getString(R.string.ps_container_unlock_button_content_description);
         mFloatingMaskViewCornerRadius = mAllApps.getContext().getResources().getDimensionPixelSize(
                 R.dimen.ps_floating_mask_corner_radius);
+        mLockTextMarginStart = mAllApps.getContext().getResources().getDimensionPixelSize(
+                R.dimen.ps_lock_icon_text_margin_start_expanded);
+        mLockTextMarginEnd = mAllApps.getContext().getResources().getDimensionPixelSize(
+                R.dimen.ps_lock_icon_text_margin_end_expanded);
     }
 
     /** Adds Private Space Header to the layout. */
@@ -209,7 +220,7 @@
      * when animation is not running.
      */
     public void reset() {
-        // Ensure the state of the header views is what it should be before animating.
+        // Ensure the state of the header view is what it should be before animating.
         updateView();
         getMainRecyclerView().setChildAttachedConsumer(null);
         int previousState = getCurrentState();
@@ -354,20 +365,12 @@
     /** Add Private Space Header view elements based upon {@link UserProfileState} */
     public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) {
         mPSHeader = parent;
+        Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Binding private space.");
+        updateView();
         if (mOnPSHeaderAdded != null) {
             MAIN_EXECUTOR.execute(mOnPSHeaderAdded);
             mOnPSHeaderAdded = null;
         }
-        // Set the transition duration for the settings and lock button to animate.
-        ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
-        if (mReadyToAnimate) {
-            enableLayoutTransition(settingAndLockGroup);
-        } else {
-            // Ensure any unwanted animations to not happen.
-            settingAndLockGroup.setLayoutTransition(null);
-            Log.d(TAG, "bindPrivateSpaceHeaderViewElements: removing transitions ");
-        }
-        updateView();
     }
 
     /** Update the states of the views that make up the header at the state it is called in. */
@@ -375,12 +378,15 @@
         if (mPSHeader == null) {
             return;
         }
+        Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: "
+                + getCurrentState());
         mPSHeader.setAlpha(1);
         ViewGroup lockPill = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
         assert lockPill != null;
-        TextView lockText = lockPill.findViewById(R.id.lock_text);
-        PrivateSpaceSettingsButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
-        assert settingsButton != null;
+        mLockText = lockPill.findViewById(R.id.lock_text);
+        assert mLockText != null;
+        mPrivateSpaceSettingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
+        assert mPrivateSpaceSettingsButton != null;
         //Add image for private space transitioning view
         ImageView transitionView = mPSHeader.findViewById(R.id.ps_transition_image);
         assert transitionView != null;
@@ -391,12 +397,19 @@
                 // Remove header from accessibility target when enabled.
                 mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
 
-                lockText.setVisibility(VISIBLE);
+                if (!mReadyToAnimate) {
+                    // Don't set visibilities when animating as the animation will handle it.
+                    mLockText.setVisibility(VISIBLE);
+                    mLockText.setAlpha(1);
+                    mLockText.setHorizontallyScrolling(false);
+                    mPrivateSpaceSettingsButton.setVisibility(
+                            isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE);
+                    mPrivateSpaceSettingsButton.setClickable(isPrivateSpaceSettingsAvailable());
+                }
                 lockPill.setVisibility(VISIBLE);
                 lockPill.setOnClickListener(view -> lockingAction(/* lock */ true));
                 lockPill.setContentDescription(mUnLockedStateContentDesc);
 
-                settingsButton.setVisibility(isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE);
                 transitionView.setVisibility(GONE);
             }
             case STATE_DISABLED -> {
@@ -406,12 +419,15 @@
                 mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
                 mPSHeader.setContentDescription(mLockedStateContentDesc);
 
-                lockText.setVisibility(GONE);
+                mLockText.setVisibility(GONE);
+                mLockText.setAlpha(0);
+                mLockText.setHorizontallyScrolling(false);
                 lockPill.setVisibility(VISIBLE);
                 lockPill.setOnClickListener(view -> lockingAction(/* lock */ false));
                 lockPill.setContentDescription(mLockedStateContentDesc);
 
-                settingsButton.setVisibility(GONE);
+                mPrivateSpaceSettingsButton.setVisibility(GONE);
+                mPrivateSpaceSettingsButton.setClickable(false);
                 transitionView.setVisibility(GONE);
             }
             case STATE_TRANSITION -> {
@@ -419,6 +435,7 @@
                 lockPill.setVisibility(GONE);
             }
         }
+        mPSHeader.invalidate();
     }
 
     /** Sets the enablement of the profile when header or button is clicked. */
@@ -458,7 +475,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) {
@@ -585,6 +603,51 @@
         return alphaAnim;
     }
 
+    private ValueAnimator animatePillTransition(boolean isExpanding) {
+        if (mLockText == null) {
+            return new ValueAnimator().setDuration(0);
+        }
+        mLockText.measure(0,0);
+        int currentWidth = mLockText.getWidth();
+        int fullWidth = mLockText.getMeasuredWidth();
+        float from = isExpanding ? 0 : currentWidth;
+        float to = isExpanding ? fullWidth : 0;
+        ValueAnimator pillAnim = ObjectAnimator.ofFloat(from, to);
+        pillAnim.setStartDelay(isExpanding ? PILL_TRANSITION_DELAY : 0);
+        pillAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        pillAnim.setInterpolator(Interpolators.STANDARD);
+        pillAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float translation = (float) valueAnimator.getAnimatedValue();
+                float translationFraction = translation / fullWidth;
+                ViewGroup.MarginLayoutParams layoutParams =
+                        (ViewGroup.MarginLayoutParams) mLockText.getLayoutParams();
+                layoutParams.width = (int) translation;
+                layoutParams.setMarginStart((int) (mLockTextMarginStart * translationFraction));
+                layoutParams.setMarginEnd((int) (mLockTextMarginEnd * translationFraction));
+                mLockText.setLayoutParams(layoutParams);
+                mLockText.requestLayout();
+            }
+        });
+        pillAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (!isExpanding) {
+                    mLockText.setVisibility(GONE);
+                }
+                mLockText.setHorizontallyScrolling(false);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mLockText.setHorizontallyScrolling(true);
+                mLockText.setVisibility(VISIBLE);
+            }
+        });
+        return pillAnim;
+    }
+
     /**
      * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an
      * animation. At the moment, collapsing, setting alpha changes, and animating the text is done
@@ -596,26 +659,13 @@
         }
         if (mPSHeader == null) {
             mOnPSHeaderAdded = () -> updatePrivateStateAnimator(expand);
-            setAnimationRunning(false);
+            // Set animation to true, because onBind will be called after this return where we want
+            // the views to be updated accordingly so animation can happen.
+            setAnimationRunning(true);
             return;
         }
         attachFloatingMaskView(expand);
-        ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
-        TextView lockText = mPSHeader.findViewById(R.id.lock_text);
-        PrivateSpaceSettingsButton privateSpaceSettingsButton =
-                mPSHeader.findViewById(R.id.ps_settings_button);
-        if (settingsAndLockGroup.getLayoutTransition() == null) {
-            // Set a new transition if the current ViewGroup does not already contain one as each
-            // transition should only happen once when applied.
-            enableLayoutTransition(settingsAndLockGroup);
-        }
-        settingsAndLockGroup.getLayoutTransition().setStartDelay(
-                LayoutTransition.CHANGING,
-                expand ? SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY : NO_DELAY);
-        PropertySetter headerSetter = new AnimatedPropertySetter();
-        headerSetter.add(updateSettingsGearAlpha(expand));
-        headerSetter.add(updateLockTextAlpha(expand));
-        AnimatorSet animatorSet = headerSetter.buildAnim();
+        AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
         animatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -626,8 +676,6 @@
                                 ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN
                                 : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN,
                         mAllApps.getActiveRecyclerView());
-                // Animate the collapsing of the text at the same time while updating lock button.
-                lockText.setVisibility(expand ? VISIBLE : GONE);
                 setAnimationRunning(true);
             }
 
@@ -646,15 +694,17 @@
                             : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END,
                     mAllApps.getActiveRecyclerView());
             Log.d(TAG, "updatePrivateStateAnimator: lockText visibility: "
-                    + lockText.getVisibility() + " lockTextAlpha: " + lockText.getAlpha());
+                    + mLockText.getVisibility() + " lockTextAlpha: " + mLockText.getAlpha());
             Log.d(TAG, "updatePrivateStateAnimator: settingsCog visibility: "
-                    + privateSpaceSettingsButton.getVisibility()
-                    + " settingsCogAlpha: " + privateSpaceSettingsButton.getAlpha());
+                    + mPrivateSpaceSettingsButton.getVisibility()
+                    + " settingsCogAlpha: " + mPrivateSpaceSettingsButton.getAlpha());
             if (!expand) {
                 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();
@@ -662,16 +712,24 @@
             }
         }));
         if (expand) {
-            animatorSet.playTogether(animateAlphaOfIcons(true),
+            animatorSet.playTogether(updateSettingsGearAlpha(true),
+                    updateLockTextAlpha(true),
+                    animateAlphaOfIcons(true),
+                    animatePillTransition(true),
                     translateFloatingMaskView(false));
         } else {
+            AnimatorSet parallelSet = new AnimatorSet();
+            parallelSet.playTogether(updateSettingsGearAlpha(false),
+                    updateLockTextAlpha(false),
+                    animateAlphaOfIcons(false),
+                    animatePillTransition(false));
             if (isPrivateSpaceHidden()) {
-                animatorSet.playSequentially(animateAlphaOfIcons(false),
+                animatorSet.playSequentially(parallelSet,
                         animateAlphaOfPrivateSpaceContainer(),
                         animateCollapseAnimation());
             } else {
                 animatorSet.playSequentially(translateFloatingMaskView(true),
-                        animateAlphaOfIcons(false),
+                        parallelSet,
                         animateCollapseAnimation());
             }
         }
@@ -702,7 +760,7 @@
     /** Fades out the private space container. */
     private ValueAnimator translateFloatingMaskView(boolean animateIn) {
         if (!Flags.privateSpaceAddFloatingMaskView() || mFloatingMaskView == null) {
-            return new ValueAnimator();
+            return new ValueAnimator().setDuration(0);
         }
         // Translate base on the height amount. Translates out on expand and in on collapse.
         float floatingMaskViewHeight = getFloatingMaskViewHeight();
@@ -714,42 +772,19 @@
         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                if (mFloatingMaskView == null) {
+                    return;
+                }
                 mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue());
             }
         });
         return alphaAnim;
     }
 
-    /** Animates the layout changes when the text of the button becomes visible/gone. */
-    private void enableLayoutTransition(ViewGroup settingsAndLockGroup) {
-        LayoutTransition settingsAndLockTransition = new LayoutTransition();
-        settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING);
-        settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION);
-        settingsAndLockTransition.setInterpolator(LayoutTransition.CHANGING,
-                Interpolators.STANDARD);
-        settingsAndLockTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
-            @Override
-            public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
-                Log.d(TAG, "updatePrivateStateAnimator: transition started: " + transition);
-            }
-            @Override
-            public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
-                settingsAndLockGroup.setLayoutTransition(null);
-                mReadyToAnimate = false;
-                Log.d(TAG, "updatePrivateStateAnimator: transition finished: " + transition);
-            }
-        });
-        settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition);
-        Log.d(TAG, "updatePrivateStateAnimator: setting transition: "
-                + settingsAndLockTransition);
-    }
-
     /** Change the settings gear alpha when expanded or collapsed. */
     private ValueAnimator updateSettingsGearAlpha(boolean expand) {
-        if (mPSHeader == null) {
-            return new ValueAnimator();
+        if (mPrivateSpaceSettingsButton == null || !isPrivateSpaceSettingsAvailable()) {
+            return new ValueAnimator().setDuration(0);
         }
         float from = expand ? 0 : 1;
         float to = expand ? 1 : 0;
@@ -760,16 +795,29 @@
         settingsAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                mPSHeader.findViewById(R.id.ps_settings_button)
-                        .setAlpha((float) valueAnimator.getAnimatedValue());
+                mPrivateSpaceSettingsButton.setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        settingsAlphaAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mPrivateSpaceSettingsButton.setVisibility(VISIBLE);
+                mPrivateSpaceSettingsButton.setClickable(false);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (expand) {
+                    mPrivateSpaceSettingsButton.setClickable(true);
+                }
             }
         });
         return settingsAlphaAnim;
     }
 
     private ValueAnimator updateLockTextAlpha(boolean expand) {
-        if (mPSHeader == null) {
-            return new ValueAnimator();
+        if (mLockText == null) {
+            return new ValueAnimator().setDuration(0);
         }
         float from = expand ? 0 : 1;
         float to = expand ? 1 : 0;
@@ -780,8 +828,7 @@
         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                mPSHeader.findViewById(R.id.lock_text).setAlpha(
-                        (float) valueAnimator.getAnimatedValue());
+                mLockText.setAlpha((float) valueAnimator.getAnimatedValue());
             }
         });
         return alphaAnim;
@@ -819,8 +866,22 @@
         if (!Flags.privateSpaceAddFloatingMaskView()) {
             return;
         }
+        // Use getLocationOnScreen() as simply checking for mPSHeader.getBottom() is only relative
+        // to its parent.
+        int[] psHeaderLocation = new int[2];
+        mPSHeader.getLocationOnScreen(psHeaderLocation);
+        int psHeaderBottomY = psHeaderLocation[1] + mPsHeaderHeight;
+        // Calculate the topY of the floatingMaskView as if it was added.
+        int floatingMaskViewBottomBoxTopY =
+                (int) (mAllApps.getBottom() - getMainRecyclerView().getPaddingBottom());
+        // Don't attach if the header will be clipped by the floating mask view.
+        if (psHeaderBottomY > floatingMaskViewBottomBoxTopY) {
+            mFloatingMaskView = null;
+            return;
+        }
         mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate(
                 R.layout.private_space_mask_view, mAllApps, false);
+        assert mFloatingMaskView != null;
         mAllApps.addView(mFloatingMaskView);
         // Translate off the screen first if its collapsing so this header view isn't visible to
         // user when animation starts.
diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
index ac9b146..eaeb8bb 100644
--- a/src/com/android/launcher3/allapps/SectionDecorationHandler.java
+++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
@@ -25,7 +25,6 @@
 import android.view.View;
 
 import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.R;
 import com.android.launcher3.util.Themes;
@@ -61,10 +60,10 @@
 
         mContext = context;
         mFillAlpha = fillAlpha;
-        mFocusColor = ContextCompat.getColor(context,
-                R.color.material_color_surface_bright); // UX recommended
-        mFillColor = ContextCompat.getColor(context,
-                R.color.material_color_surface_container_high); // UX recommended
+        mFocusColor = Themes.getAttrColor(context,
+                R.attr.materialColorSurfaceBright); // UX recommended
+        mFillColor = Themes.getAttrColor(context,
+                R.attr.materialColorSurfaceContainerHigh); // UX recommended
 
         mIsTopLeftRound = isTopLeftRound;
         mIsTopRightRound = isTopRightRound;
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
deleted file mode 100644
index 6049574..0000000
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.allapps;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.WindowInsets;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.core.graphics.Insets;
-import androidx.core.view.WindowInsetsCompat;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
-import com.android.launcher3.model.StringCache;
-import com.android.launcher3.views.ActivityContext;
-/**
- * Work profile toggle switch shown at the bottom of AllApps work tab
- */
-public class WorkModeSwitch extends LinearLayout implements Insettable,
-        KeyboardInsetAnimationCallback.KeyboardInsetListener {
-
-    private static final int FLAG_FADE_ONGOING = 1 << 1;
-    private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
-    private static final int FLAG_PROFILE_TOGGLE_ONGOING = 1 << 3;
-    private static final int SCROLL_THRESHOLD_DP = 10;
-
-    private final Rect mInsets = new Rect();
-    private final Rect mImeInsets = new Rect();
-    private int mFlags;
-    private final ActivityContext mActivityContext;
-    private final Context mContext;
-
-    // Threshold when user scrolls up/down to determine when should button extend/collapse
-    private final int mScrollThreshold;
-    private TextView mTextView;
-
-
-    public WorkModeSwitch(@NonNull Context context) {
-        this(context, null, 0);
-    }
-
-    public WorkModeSwitch(@NonNull Context context, @NonNull AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public WorkModeSwitch(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mContext = context;
-        mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
-        mActivityContext = ActivityContext.lookupContext(getContext());
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        mTextView = findViewById(R.id.pause_text);
-        setSelected(true);
-        KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
-                new KeyboardInsetAnimationCallback(this);
-        setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
-
-        setInsets(mActivityContext.getDeviceProfile().getInsets());
-        updateStringFromCache();
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        mInsets.set(insets);
-        updateTranslationY();
-        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
-        if (lp != null) {
-            int bottomMargin = getResources().getDimensionPixelSize(R.dimen.work_fab_margin_bottom);
-            DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
-            if (mActivityContext.getAppsView().isSearchBarFloating()) {
-                bottomMargin += dp.hotseatQsbHeight;
-            }
-
-            if (!dp.isGestureMode && dp.isTaskbarPresent) {
-                bottomMargin += dp.taskbarHeight;
-            }
-
-            lp.bottomMargin = bottomMargin;
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        boolean isRtl = Utilities.isRtl(getResources());
-        int shift = mActivityContext.getDeviceProfile().getAllAppsIconStartMargin(mContext);
-        setTranslationX(isRtl ? shift : -shift);
-    }
-
-    @Override
-    public boolean isEnabled() {
-        return super.isEnabled() && getVisibility() == VISIBLE && mFlags == 0;
-    }
-
-    public void animateVisibility(boolean visible) {
-        clearAnimation();
-        if (visible) {
-            setFlag(FLAG_FADE_ONGOING);
-            setVisibility(VISIBLE);
-            extend();
-            animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
-        } else if (getVisibility() != GONE) {
-            setFlag(FLAG_FADE_ONGOING);
-            animate().alpha(0).withEndAction(() -> {
-                removeFlag(FLAG_FADE_ONGOING);
-                setVisibility(GONE);
-            }).start();
-        }
-    }
-
-    @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        WindowInsetsCompat windowInsetsCompat =
-                WindowInsetsCompat.toWindowInsetsCompat(insets, this);
-        if (windowInsetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
-            setInsets(mImeInsets, windowInsetsCompat.getInsets(WindowInsetsCompat.Type.ime()));
-        } else {
-            mImeInsets.setEmpty();
-        }
-        updateTranslationY();
-        return super.onApplyWindowInsets(insets);
-    }
-
-    void updateTranslationY() {
-        setTranslationY(-mImeInsets.bottom);
-    }
-
-    @Override
-    public void setTranslationY(float translationY) {
-        // Always translate at least enough for nav bar insets.
-        super.setTranslationY(Math.min(translationY, -mInsets.bottom));
-    }
-
-    private void setInsets(Rect rect, Insets insets) {
-        rect.set(insets.left, insets.top, insets.right, insets.bottom);
-    }
-
-    public Rect getImeInsets() {
-        return mImeInsets;
-    }
-
-    @Override
-    public void onTranslationStart() {
-        setFlag(FLAG_TRANSLATION_ONGOING);
-    }
-
-    @Override
-    public void onTranslationEnd() {
-        removeFlag(FLAG_TRANSLATION_ONGOING);
-    }
-
-    private void setFlag(int flag) {
-        mFlags |= flag;
-    }
-
-    private void removeFlag(int flag) {
-        mFlags &= ~flag;
-    }
-
-    public void extend() {
-        mTextView.setVisibility(VISIBLE);
-    }
-
-    public void shrink(){
-        mTextView.setVisibility(GONE);
-    }
-
-    public int getScrollThreshold() {
-        return mScrollThreshold;
-    }
-
-    public void updateStringFromCache(){
-        StringCache cache = mActivityContext.getStringCache();
-        if (cache != null) {
-            mTextView.setText(cache.workProfilePauseButton);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 96998a3..3d0c1d0 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -58,7 +58,7 @@
         implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener {
     private static final String TAG = "WorkProfileManager";
     private final ActivityAllAppsContainerView<?> mAllApps;
-    private WorkModeSwitch mWorkModeSwitch;
+    private WorkUtilityView mWorkUtilityView;
     private final Predicate<UserHandle> mWorkProfileMatcher;
 
     public WorkProfileManager(
@@ -79,15 +79,15 @@
 
     @Override
     public void onActivePageChanged(int page) {
-        updateWorkFAB(page);
+        updateWorkUtilityViews(page);
     }
 
-    private void updateWorkFAB(int page) {
-        if (mWorkModeSwitch != null) {
+    private void updateWorkUtilityViews(int page) {
+        if (mWorkUtilityView != null) {
             if (page == MAIN || page == SEARCH) {
-                mWorkModeSwitch.animateVisibility(false);
+                mWorkUtilityView.animateVisibility(false);
             } else if (page == WORK && getCurrentState() == STATE_ENABLED) {
-                mWorkModeSwitch.animateVisibility(true);
+                mWorkUtilityView.animateVisibility(true);
             }
         }
     }
@@ -104,10 +104,10 @@
         }
         boolean isEnabled = !mAllApps.getAppsStore().hasModelFlag(quietModeFlag);
         updateCurrentState(isEnabled ? STATE_ENABLED : STATE_DISABLED);
-        if (mWorkModeSwitch != null) {
+        if (mWorkUtilityView != null) {
             // reset the position of the button and clear IME insets.
-            mWorkModeSwitch.getImeInsets().setEmpty();
-            mWorkModeSwitch.updateTranslationY();
+            mWorkUtilityView.getImeInsets().setEmpty();
+            mWorkUtilityView.updateTranslationY();
         }
     }
 
@@ -116,54 +116,54 @@
         if (getAH() != null) {
             getAH().mAppsList.updateAdapterItems();
         }
-        if (mWorkModeSwitch != null) {
-            updateWorkFAB(mAllApps.getCurrentPage());
+        if (mWorkUtilityView != null) {
+            updateWorkUtilityViews(mAllApps.getCurrentPage());
         }
         if (getCurrentState() == STATE_ENABLED) {
-            attachWorkModeSwitch();
+            attachWorkUtilityViews();
         } else if (getCurrentState() == STATE_DISABLED) {
-            detachWorkModeSwitch();
+            detachWorkUtilityViews();
         }
     }
 
     /**
      * Creates and attaches for profile toggle button to {@link ActivityAllAppsContainerView}
      */
-    public boolean attachWorkModeSwitch() {
+    public boolean attachWorkUtilityViews() {
         if (!mAllApps.getAppsStore().hasModelFlag(
                 FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION)) {
             Log.e(TAG, "unable to attach work mode switch; Missing required permissions");
             return false;
         }
-        if (mWorkModeSwitch == null) {
-            mWorkModeSwitch = (WorkModeSwitch) mAllApps.getLayoutInflater().inflate(
-                    R.layout.work_mode_fab, mAllApps, false);
+        if (mWorkUtilityView == null) {
+            mWorkUtilityView = (WorkUtilityView) mAllApps.getLayoutInflater().inflate(
+                    R.layout.work_mode_utility_view, mAllApps, false);
         }
-        if (mWorkModeSwitch.getParent() == null) {
-            mAllApps.addView(mWorkModeSwitch);
+        if (mWorkUtilityView.getParent() == null) {
+            mAllApps.addView(mWorkUtilityView);
         }
         if (mAllApps.getCurrentPage() != WORK) {
-            mWorkModeSwitch.animateVisibility(false);
+            mWorkUtilityView.animateVisibility(false);
         }
         if (getAH() != null) {
             getAH().applyPadding();
         }
-        mWorkModeSwitch.setOnClickListener(this::onWorkFabClicked);
+        mWorkUtilityView.setOnClickListener(this::onWorkFabClicked);
         return true;
     }
     /**
      * Removes work profile toggle button from {@link ActivityAllAppsContainerView}
      */
-    public void detachWorkModeSwitch() {
-        if (mWorkModeSwitch != null && mWorkModeSwitch.getParent() == mAllApps) {
-            mAllApps.removeView(mWorkModeSwitch);
+    public void detachWorkUtilityViews() {
+        if (mWorkUtilityView != null && mWorkUtilityView.getParent() == mAllApps) {
+            mAllApps.removeView(mWorkUtilityView);
         }
-        mWorkModeSwitch = null;
+        mWorkUtilityView = null;
     }
 
     @Nullable
-    public WorkModeSwitch getWorkModeSwitch() {
-        return mWorkModeSwitch;
+    public WorkUtilityView getWorkUtilityView() {
+        return mWorkUtilityView;
     }
 
     private ActivityAllAppsContainerView.AdapterHolder getAH() {
@@ -199,7 +199,7 @@
     }
 
     private void onWorkFabClicked(View view) {
-        if (getCurrentState() == STATE_ENABLED && mWorkModeSwitch.isEnabled()) {
+        if (getCurrentState() == STATE_ENABLED && mWorkUtilityView.isEnabled()) {
             logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP);
             setWorkProfileEnabled(false);
         }
@@ -216,7 +216,7 @@
             }
             @Override
             public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
-                WorkModeSwitch fab = getWorkModeSwitch();
+                WorkUtilityView fab = getWorkUtilityView();
                 if (fab == null){
                     return;
                 }
diff --git a/src/com/android/launcher3/allapps/WorkUtilityView.java b/src/com/android/launcher3/allapps/WorkUtilityView.java
new file mode 100644
index 0000000..4b58ab0
--- /dev/null
+++ b/src/com/android/launcher3/allapps/WorkUtilityView.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.graphics.Insets;
+import androidx.core.view.WindowInsetsCompat;
+
+import com.android.app.animation.Interpolators;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatedPropertySetter;
+import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
+import com.android.launcher3.model.StringCache;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.ArrayList;
+
+/**
+ * Work profile utility ViewGroup that is shown at the bottom of AllApps work tab
+ */
+public class WorkUtilityView extends LinearLayout implements Insettable,
+        KeyboardInsetAnimationCallback.KeyboardInsetListener {
+
+    private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
+    private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
+    private static final int EXPAND_COLLAPSE_DURATION = 300;
+    private static final int TEXT_ALPHA_EXPAND_DELAY = 80;
+    private static final int TEXT_ALPHA_COLLAPSE_DELAY = 0;
+    private static final int WORK_SCHEDULER_OPACITY_DURATION =
+            (int) (EXPAND_COLLAPSE_DURATION * 0.75f);
+    private static final int FLAG_FADE_ONGOING = 1 << 1;
+    private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
+    private static final int FLAG_IS_EXPAND = 1 << 3;
+    private static final int SCROLL_THRESHOLD_DP = 10;
+    private static final float WORK_SCHEDULER_SCALE_MIN = 0.25f;
+    private static final float WORK_SCHEDULER_SCALE_MAX = 1f;
+
+    private final Rect mInsets = new Rect();
+    private final Rect mImeInsets = new Rect();
+    private int mFlags;
+    private final ActivityContext mActivityContext;
+    private final Context mContext;
+    private final int mTextMarginStart;
+    private final int mTextMarginEnd;
+    private final int mIconMarginStart;
+    private final String mWorkSchedulerIntentAction;
+
+    // Threshold when user scrolls up/down to determine when should button extend/collapse
+    private final int mScrollThreshold;
+    private ValueAnimator mPauseFABAnim;
+    private TextView mPauseText;
+    private ImageView mWorkIcon;
+    private ImageButton mSchedulerButton;
+
+    public WorkUtilityView(@NonNull Context context) {
+        this(context, null, 0);
+    }
+
+    public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContext = context;
+        mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
+        mActivityContext = ActivityContext.lookupContext(getContext());
+        mTextMarginStart = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_text_start_margin);
+        mTextMarginEnd = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_text_end_margin);
+        mIconMarginStart = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_icon_start_margin_expanded);
+        mWorkSchedulerIntentAction = mContext.getResources().getString(
+                R.string.work_profile_scheduler_intent);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mPauseText = findViewById(R.id.pause_text);
+        mWorkIcon = findViewById(R.id.work_icon);
+        mSchedulerButton = findViewById(R.id.work_scheduler);
+        setSelected(true);
+        KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
+                new KeyboardInsetAnimationCallback(this);
+        setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
+        // Expand is the default state upon initialization.
+        addFlag(FLAG_IS_EXPAND);
+        setInsets(mActivityContext.getDeviceProfile().getInsets());
+        updateStringFromCache();
+        mSchedulerButton.setVisibility(GONE);
+        if (shouldUseScheduler()) {
+            mSchedulerButton.setVisibility(VISIBLE);
+            mSchedulerButton.setOnClickListener(view ->
+                    mContext.startActivity(new Intent(mWorkSchedulerIntentAction)));
+        }
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+        updateTranslationY();
+        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
+        if (lp != null) {
+            int bottomMargin = getResources().getDimensionPixelSize(R.dimen.work_fab_margin_bottom);
+            DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
+            if (mActivityContext.getAppsView().isSearchBarFloating()) {
+                bottomMargin += dp.hotseatQsbHeight;
+            }
+
+            if (!dp.isGestureMode && dp.isTaskbarPresent) {
+                bottomMargin += dp.taskbarHeight;
+            }
+
+            lp.bottomMargin = bottomMargin;
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        boolean isRtl = Utilities.isRtl(getResources());
+        int shift = mActivityContext.getDeviceProfile().getAllAppsIconStartMargin(mContext);
+        setTranslationX(isRtl ? shift : -shift);
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return super.isEnabled() && getVisibility() == VISIBLE;
+    }
+
+    public void animateVisibility(boolean visible) {
+        clearAnimation();
+        if (visible) {
+            addFlag(FLAG_FADE_ONGOING);
+            setVisibility(VISIBLE);
+            extend();
+            animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
+        } else if (getVisibility() != GONE) {
+            addFlag(FLAG_FADE_ONGOING);
+            animate().alpha(0).withEndAction(() -> {
+                removeFlag(FLAG_FADE_ONGOING);
+                setVisibility(GONE);
+            }).start();
+        }
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        WindowInsetsCompat windowInsetsCompat =
+                WindowInsetsCompat.toWindowInsetsCompat(insets, this);
+        if (windowInsetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
+            setInsets(mImeInsets, windowInsetsCompat.getInsets(WindowInsetsCompat.Type.ime()));
+            shrink();
+        } else {
+            mImeInsets.setEmpty();
+            extend();
+        }
+        updateTranslationY();
+        return super.onApplyWindowInsets(insets);
+    }
+
+    void updateTranslationY() {
+        setTranslationY(-mImeInsets.bottom);
+    }
+
+    @Override
+    public void setTranslationY(float translationY) {
+        // Always translate at least enough for nav bar insets.
+        super.setTranslationY(Math.min(translationY, -mInsets.bottom));
+    }
+
+    private ValueAnimator animateSchedulerScale(boolean isExpanding) {
+        float scaleFrom = isExpanding ? WORK_SCHEDULER_SCALE_MIN : WORK_SCHEDULER_SCALE_MAX;
+        float scaleTo = isExpanding ? WORK_SCHEDULER_SCALE_MAX : WORK_SCHEDULER_SCALE_MIN;
+        ValueAnimator schedulerScaleAnim = ObjectAnimator.ofFloat(scaleFrom, scaleTo);
+        schedulerScaleAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        schedulerScaleAnim.setInterpolator(Interpolators.STANDARD);
+        schedulerScaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float scale = (float) valueAnimator.getAnimatedValue();
+                mSchedulerButton.setScaleX(scale);
+                mSchedulerButton.setScaleY(scale);
+            }
+        });
+        schedulerScaleAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                if (isExpanding) {
+                    mSchedulerButton.setVisibility(VISIBLE);
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!isExpanding) {
+                    mSchedulerButton.setVisibility(GONE);
+                }
+            }
+        });
+        return schedulerScaleAnim;
+    }
+
+    private ValueAnimator animateSchedulerAlpha(boolean isExpanding) {
+        float alphaFrom = isExpanding ? 0 : 1;
+        float alphaTo = isExpanding ? 1 : 0;
+        ValueAnimator schedulerAlphaAnim = ObjectAnimator.ofFloat(alphaFrom, alphaTo);
+        schedulerAlphaAnim.setDuration(WORK_SCHEDULER_OPACITY_DURATION);
+        schedulerAlphaAnim.setStartDelay(isExpanding ? 0 :
+                EXPAND_COLLAPSE_DURATION - WORK_SCHEDULER_OPACITY_DURATION);
+        schedulerAlphaAnim.setInterpolator(Interpolators.STANDARD);
+        schedulerAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mSchedulerButton.setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        return schedulerAlphaAnim;
+    }
+
+    private void animateWorkUtilityViews(boolean isExpanding) {
+        if (!shouldAnimate(isExpanding)) {
+            return;
+        }
+        AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
+        mPauseText.measure(0,0);
+        int currentWidth = mPauseText.getWidth();
+        int fullWidth = mPauseText.getMeasuredWidth();
+        float from = isExpanding ? 0 : currentWidth;
+        float to = isExpanding ? fullWidth : 0;
+        mPauseFABAnim = ObjectAnimator.ofFloat(from, to);
+        mPauseFABAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        mPauseFABAnim.setInterpolator(Interpolators.STANDARD);
+        mPauseFABAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float translation = (float) valueAnimator.getAnimatedValue();
+                float translationFraction = translation / fullWidth;
+                ViewGroup.MarginLayoutParams textViewLayoutParams =
+                        (ViewGroup.MarginLayoutParams) mPauseText.getLayoutParams();
+                textViewLayoutParams.width = (int) translation;
+                textViewLayoutParams.setMarginStart((int) (mTextMarginStart * translationFraction));
+                textViewLayoutParams.setMarginEnd((int) (mTextMarginEnd * translationFraction));
+                mPauseText.setLayoutParams(textViewLayoutParams);
+                ViewGroup.MarginLayoutParams iconLayoutParams =
+                        (ViewGroup.MarginLayoutParams) mWorkIcon.getLayoutParams();
+                iconLayoutParams.setMarginStart((int) (mIconMarginStart * translationFraction));
+                mWorkIcon.setLayoutParams(iconLayoutParams);
+            }
+        });
+        mPauseFABAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (isExpanding) {
+                    addFlag(FLAG_IS_EXPAND);
+                } else {
+                    mPauseText.setVisibility(GONE);
+                    removeFlag(FLAG_IS_EXPAND);
+                }
+                mPauseText.setHorizontallyScrolling(false);
+                mPauseText.setEllipsize(TextUtils.TruncateAt.END);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mPauseText.setHorizontallyScrolling(true);
+                mPauseText.setVisibility(VISIBLE);
+                mPauseText.setEllipsize(null);
+            }
+        });
+        ArrayList<Animator> animatorList = new ArrayList<>();
+        animatorList.add(mPauseFABAnim);
+        animatorList.add(updatePauseTextAlpha(isExpanding));
+        if (shouldUseScheduler()) {
+            animatorList.add(animateSchedulerScale(isExpanding));
+            animatorList.add(animateSchedulerAlpha(isExpanding));
+        }
+        animatorSet.playTogether(animatorList);
+        animatorSet.start();
+    }
+
+
+    private ValueAnimator updatePauseTextAlpha(boolean expand) {
+        float from = expand ? 0 : 1;
+        float to = expand ? 1 : 0;
+        ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
+        alphaAnim.setDuration(expand ? TEXT_EXPAND_OPACITY_DURATION
+                : TEXT_COLLAPSE_OPACITY_DURATION);
+        alphaAnim.setStartDelay(expand ? TEXT_ALPHA_EXPAND_DELAY : TEXT_ALPHA_COLLAPSE_DELAY);
+        alphaAnim.setInterpolator(Interpolators.LINEAR);
+        alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mPauseText.setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        return alphaAnim;
+    }
+
+    private void setInsets(Rect rect, Insets insets) {
+        rect.set(insets.left, insets.top, insets.right, insets.bottom);
+    }
+
+    public Rect getImeInsets() {
+        return mImeInsets;
+    }
+
+    @Override
+    public void onTranslationStart() {
+        addFlag(FLAG_TRANSLATION_ONGOING);
+    }
+
+    @Override
+    public void onTranslationEnd() {
+        removeFlag(FLAG_TRANSLATION_ONGOING);
+    }
+
+    private void addFlag(int flag) {
+        mFlags |= flag;
+    }
+
+    private void removeFlag(int flag) {
+        mFlags &= ~flag;
+    }
+
+    private boolean containsFlag(int flag) {
+        return (mFlags & flag) == flag;
+    }
+
+    public void extend() {
+        animateWorkUtilityViews(true);
+    }
+
+    public void shrink() {
+        animateWorkUtilityViews(false);
+    }
+
+    /**
+     * Determines if the button should animate based on current state. It should animate the button
+     * only if it is not in the same state it is animating to.
+     */
+    private boolean shouldAnimate(boolean expanding) {
+        return expanding != containsFlag(FLAG_IS_EXPAND)
+                && (mPauseFABAnim == null || !mPauseFABAnim.isRunning());
+    }
+
+    public int getScrollThreshold() {
+        return mScrollThreshold;
+    }
+
+    public void updateStringFromCache(){
+        StringCache cache = mActivityContext.getStringCache();
+        if (cache != null) {
+            mPauseText.setText(cache.workProfilePauseButton);
+        }
+    }
+
+    private boolean shouldUseScheduler() {
+        return Flags.workSchedulerInWorkProfile() && !mWorkSchedulerIntentAction.isEmpty();
+    }
+}
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index ec45415..7e3e392 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -22,8 +22,6 @@
 import android.text.style.SuggestionSpan;
 import android.util.Log;
 import android.view.KeyEvent;
-import android.view.View;
-import android.view.View.OnFocusChangeListener;
 import android.view.inputmethod.EditorInfo;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
@@ -31,7 +29,6 @@
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.search.SearchAlgorithm;
 import com.android.launcher3.search.SearchCallback;
 import com.android.launcher3.views.ActivityContext;
@@ -40,8 +37,7 @@
  * An interface to a search box that AllApps can command.
  */
 public class AllAppsSearchBarController
-        implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener,
-        OnFocusChangeListener {
+        implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener {
 
     private static final String TAG = "AllAppsSearchBarController";
     protected ActivityContext mLauncher;
@@ -69,7 +65,6 @@
         mInput.addTextChangedListener(this);
         mInput.setOnEditorActionListener(this);
         mInput.setOnBackKeyListener(this);
-        mInput.addOnFocusChangeListener(this);
         mSearchAlgorithm = searchAlgorithm;
     }
 
@@ -123,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();
         }
@@ -142,13 +143,6 @@
         return false;
     }
 
-    @Override
-    public void onFocusChange(View view, boolean hasFocus) {
-        if (!hasFocus && !FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-            mInput.hideKeyboard();
-        }
-    }
-
     /**
      * Resets the search bar state.
      */
@@ -157,7 +151,6 @@
         mInput.reset();
         mInput.clearFocus();
         mQuery = null;
-        mInput.removeOnFocusChangeListener(this);
     }
 
     /**
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/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
index 4f8d53e..d593f80 100644
--- a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
+++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
@@ -3,6 +3,7 @@
 import android.content.Context;
 import android.icu.text.AlphabeticIndex;
 import android.os.LocaleList;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 
@@ -12,6 +13,9 @@
 
 public class AlphabeticIndexCompat {
 
+    // TODO(b/336947811): Set to false after root causing is done.
+    private static final boolean DEBUG = true;
+    private static final String TAG = "AlphabeticIndexCompat";
     private static final String MID_DOT = "\u2219";
     private final String mDefaultMiscLabel;
 
@@ -49,6 +53,9 @@
     public String computeSectionName(@NonNull CharSequence cs) {
         String s = Utilities.trim(cs);
         String sectionName = mBaseIndex.getBucket(mBaseIndex.getBucketIndex(s)).getLabel();
+        if (DEBUG) {
+            Log.d(TAG, "computeSectionName: cs: " + cs + " sectionName: " + sectionName);
+        }
         if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) {
             int c = s.codePointAt(0);
             boolean startsWithDigit = Character.isDigit(c);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index d0596fa..9e38824 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -62,54 +62,7 @@
      * and set a default value for the flag. This will be the default value on Debug builds.
      * <p>
      */
-    // TODO(Block 2): Clean up flags
-    public static final BooleanFlag ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH = getDebugFlag(270395073,
-            "ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH", DISABLED,
-            "Allow bottom sheet depth to be smaller than 1 for multi-display devices.");
-
-    // TODO(Block 3): Clean up flags
-    public static final BooleanFlag ENABLE_DISMISS_PREDICTION_UNDO = getDebugFlag(270394476,
-            "ENABLE_DISMISS_PREDICTION_UNDO", DISABLED,
-            "Show an 'Undo' snackbar when users dismiss a predicted hotseat item");
-    public static final BooleanFlag CONTINUOUS_VIEW_TREE_CAPTURE = getDebugFlag(270395171,
-            "CONTINUOUS_VIEW_TREE_CAPTURE", ENABLED, "Capture View tree every frame");
-
-    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 4): Cleanup flags
-    public static final BooleanFlag ENABLE_ALL_APPS_FROM_OVERVIEW =
-            getDebugFlag(275132633, "ENABLE_ALL_APPS_FROM_OVERVIEW", DISABLED,
-                    "Allow entering All Apps from Overview (e.g. long swipe up from app)");
-
-    public static final BooleanFlag ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS = getReleaseFlag(
-            270394468, "ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS", ENABLED,
-            "Enable option to show keyboard when going to all-apps");
-
-    // TODO(Block 5): Clean up flags
-    public static final BooleanFlag ENABLE_TWOLINE_DEVICESEARCH = getDebugFlag(201388851,
-            "ENABLE_TWOLINE_DEVICESEARCH", DISABLED,
-            "Enable two line label for icons with labels on device search.");
-
-    public static final BooleanFlag ENABLE_ICON_IN_TEXT_HEADER = getDebugFlag(270395143,
-            "ENABLE_ICON_IN_TEXT_HEADER", DISABLED, "Show icon in textheader");
-
-    public static final BooleanFlag ENABLE_PREMIUM_HAPTICS_ALL_APPS = getDebugFlag(270396358,
-            "ENABLE_PREMIUM_HAPTICS_ALL_APPS", DISABLED,
-            "Enables haptics opening/closing All apps");
-
     // TODO(Block 6): Clean up flags
-    public static final BooleanFlag ENABLE_ALL_APPS_SEARCH_IN_TASKBAR = getDebugFlag(270393900,
-            "ENABLE_ALL_APPS_SEARCH_IN_TASKBAR", ENABLED,
-            "Enables Search box in Taskbar All Apps.");
-
     public static final BooleanFlag SECONDARY_DRAG_N_DROP_TO_PIN = getDebugFlag(270395140,
             "SECONDARY_DRAG_N_DROP_TO_PIN", DISABLED,
             "Enable dragging and dropping to pin apps within secondary display");
@@ -125,10 +78,6 @@
     public static final BooleanFlag FOLDABLE_SINGLE_PAGE = getDebugFlag(270395274,
             "FOLDABLE_SINGLE_PAGE", DISABLED, "Use a single page for the workspace");
 
-    public static final BooleanFlag ENABLE_PARAMETRIZE_REORDER = getDebugFlag(289420844,
-            "ENABLE_PARAMETRIZE_REORDER", DISABLED,
-            "Enables generating the reorder using a set of parameters");
-
     // TODO(Block 12): Clean up flags
     public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag(270396680,
             "ENABLE_MULTI_INSTANCE", DISABLED,
@@ -175,32 +124,11 @@
     public static final BooleanFlag PROMISE_APPS_IN_ALL_APPS = getDebugFlag(270390012,
             "PROMISE_APPS_IN_ALL_APPS", DISABLED, "Add promise icon in all-apps");
 
-    public static final BooleanFlag KEYGUARD_ANIMATION = getDebugFlag(270390904,
-            "KEYGUARD_ANIMATION", DISABLED,
-            "Enable animation for keyguard going away on wallpaper");
-
-    public static final BooleanFlag ENABLE_DEVICE_SEARCH = getReleaseFlag(270390907,
-            "ENABLE_DEVICE_SEARCH", ENABLED, "Allows on device search in all apps");
-
-    public static final BooleanFlag ENABLE_HIDE_HEADER = getReleaseFlag(270390930,
-            "ENABLE_HIDE_HEADER", ENABLED, "Hide header on keyboard before typing in all apps");
-
     // Aconfig migration complete for ENABLE_EXPANDING_PAUSE_WORK_BUTTON.
     public static final BooleanFlag ENABLE_EXPANDING_PAUSE_WORK_BUTTON = getDebugFlag(270390779,
             "ENABLE_EXPANDING_PAUSE_WORK_BUTTON", DISABLED,
             "Expand and collapse pause work button while scrolling");
 
-    // Aconfig migration complete for ENABLE_TWOLINE_ALLAPPS.
-    public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937,
-            "ENABLE_TWOLINE_ALLAPPS", DISABLED, "Enables two line label inside all apps.");
-
-    public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(270391693,
-            "IME_STICKY_SNACKBAR_EDU", ENABLED, "Show sticky IME edu in AllApps");
-
-    public static final BooleanFlag FOLDER_NAME_MAJORITY_RANKING = getDebugFlag(270391638,
-            "FOLDER_NAME_MAJORITY_RANKING", ENABLED,
-            "Suggests folder names based on majority based ranking.");
-
     public static final BooleanFlag INJECT_FALLBACK_APP_CORPUS_RESULTS = getReleaseFlag(270391706,
             "INJECT_FALLBACK_APP_CORPUS_RESULTS", DISABLED,
             "Inject fallback app corpus result when AiAi fails to return it.");
@@ -225,27 +153,7 @@
         return ENABLE_APP_PAIRS.get() || com.android.wm.shell.Flags.enableAppPairs();
     }
 
-    // TODO(Block 19): Clean up flags
-    public static final BooleanFlag SCROLL_TOP_TO_RESET = getReleaseFlag(270395177,
-            "SCROLL_TOP_TO_RESET", ENABLED,
-            "Bring up IME and focus on input when scroll to top if 'Always show keyboard'"
-                    + " is enabled or in prefix state");
-
-    public static final BooleanFlag ENABLE_SEARCH_UNINSTALLED_APPS = getReleaseFlag(270395269,
-            "ENABLE_SEARCH_UNINSTALLED_APPS", ENABLED, "Search uninstalled app results.");
-
     // TODO(Block 20): Clean up flags
-    public static final BooleanFlag ENABLE_SCRIM_FOR_APP_LAUNCH = getDebugFlag(270393276,
-            "ENABLE_SCRIM_FOR_APP_LAUNCH", DISABLED, "Enables scrim during app launch animation.");
-
-    public static final BooleanFlag ENABLE_BACK_SWIPE_HOME_ANIMATION = getDebugFlag(270393426,
-            "ENABLE_BACK_SWIPE_HOME_ANIMATION", ENABLED,
-            "Enables home animation to icon when user swipes back.");
-
-    public static final BooleanFlag ENABLE_DYNAMIC_TASKBAR_THRESHOLDS = getDebugFlag(294252473,
-            "ENABLE_DYNAMIC_TASKBAR_THRESHOLDS", ENABLED,
-            "Enables taskbar thresholds that scale based on screen size.");
-
     // Aconfig migration complete for ENABLE_HOME_TRANSITION_LISTENER.
     public static final BooleanFlag ENABLE_HOME_TRANSITION_LISTENER = getDebugFlag(306053414,
             "ENABLE_HOME_TRANSITION_LISTENER", DISABLED,
@@ -264,18 +172,7 @@
             "ENABLE_WIDGET_TRANSITION_FOR_RESIZING", DISABLED,
             "Enable widget transition animation when resizing the widgets");
 
-    public static final BooleanFlag PREEMPTIVE_UNFOLD_ANIMATION_START = getDebugFlag(270397209,
-            "PREEMPTIVE_UNFOLD_ANIMATION_START", ENABLED,
-            "Enables starting the unfold animation preemptively when unfolding, without"
-                    + "waiting for SystemUI and then merging the SystemUI progress whenever we "
-                    + "start receiving the events");
-
     // TODO(Block 25): Clean up flags
-    public static final BooleanFlag ENABLE_NEW_GESTURE_NAV_TUTORIAL = getDebugFlag(270396257,
-            "ENABLE_NEW_GESTURE_NAV_TUTORIAL", ENABLED,
-            "Enable the redesigned gesture navigation tutorial");
-
-    // TODO(Block 26): Clean up flags
     public static final BooleanFlag ENABLE_WIDGET_HOST_IN_BACKGROUND = getDebugFlag(270394384,
             "ENABLE_WIDGET_HOST_IN_BACKGROUND", ENABLED,
             "Enable background widget updates listening for widget holder");
@@ -300,10 +197,6 @@
             "SEPARATE_RECENTS_ACTIVITY", DISABLED,
             "Uses a separate recents activity instead of using the integrated recents+Launcher UI");
 
-    public static final BooleanFlag ENABLE_ENFORCED_ROUNDED_CORNERS = getReleaseFlag(270393258,
-            "ENABLE_ENFORCED_ROUNDED_CORNERS", ENABLED,
-            "Enforce rounded corners on all App Widgets");
-
     public static final BooleanFlag USE_LOCAL_ICON_OVERRIDES = getDebugFlag(270394973,
             "USE_LOCAL_ICON_OVERRIDES", ENABLED,
             "Use inbuilt monochrome icons if app doesn't provide one");
@@ -317,20 +210,15 @@
                 com.android.wm.shell.Flags.enableSplitContextual();
     }
 
-    public static final BooleanFlag ENABLE_TRACKPAD_GESTURE = getDebugFlag(271010401,
-            "ENABLE_TRACKPAD_GESTURE", ENABLED, "Enables trackpad gesture.");
-
     // TODO(Block 29): Clean up flags
+    // Aconfig migration complete for ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.
     public static final BooleanFlag ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT = getDebugFlag(270393897,
             "ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT", DISABLED,
             "Enables displaying the all apps button in the hotseat.");
 
-    public static final BooleanFlag ENABLE_KEYBOARD_QUICK_SWITCH = getDebugFlag(270396844,
-            "ENABLE_KEYBOARD_QUICK_SWITCH", ENABLED, "Enables keyboard quick switching");
-
-    public static final BooleanFlag ENABLE_KEYBOARD_TASKBAR_TOGGLE = getDebugFlag(281726846,
-            "ENABLE_KEYBOARD_TASKBAR_TOGGLE", ENABLED,
-            "Enables keyboard taskbar stash toggling");
+    public static boolean enableAllAppsButtonInHotseat() {
+        return ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get() || Flags.enableAllAppsButtonInHotseat();
+    }
 
     // TODO(Block 30): Clean up flags
     public static final BooleanFlag USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES = getDebugFlag(270395010,
@@ -349,14 +237,6 @@
         return ENABLE_RESPONSIVE_WORKSPACE.get() || Flags.enableResponsiveWorkspace();
     }
 
-    // TODO(Block 33): Clean up flags
-    public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355,
-            "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED,
-            "Enables preinflating all apps icons to avoid scrolling jank.");
-    public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514,
-            "ALL_APPS_GONE_VISIBILITY", ENABLED,
-            "Set all apps container view's hidden visibility to GONE instead of INVISIBLE.");
-
     public static BooleanFlag getDebugFlag(
             int bugId, String key, BooleanFlag flagState, String description) {
         return flagState;
diff --git a/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java b/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java
new file mode 100644
index 0000000..5664174
--- /dev/null
+++ b/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 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.contextualeducation;
+
+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 through dagger modules to provide a real implementation.
+ */
+@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
+     * @param isTrackpadGesture indicates if the gesture is triggered by trackpad
+     * @param gestureType type of gesture triggered
+     */
+    public void updateEduStats(boolean isTrackpadGesture, GestureType gestureType) {
+    }
+}
diff --git a/src/com/android/launcher3/dagger/ActivityContextScope.java b/src/com/android/launcher3/dagger/ActivityContextScope.java
new file mode 100644
index 0000000..887f15c
--- /dev/null
+++ b/src/com/android/launcher3/dagger/ActivityContextScope.java
@@ -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.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Scope;
+
+/**
+ * Scope annotation for singletons associated with Launcher activity context.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Scope
+public @interface ActivityContextScope {
+}
diff --git a/src/com/android/launcher3/dagger/ApplicationContext.java b/src/com/android/launcher3/dagger/ApplicationContext.java
new file mode 100644
index 0000000..9a5b08b
--- /dev/null
+++ b/src/com/android/launcher3/dagger/ApplicationContext.java
@@ -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.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Qualifier for Launcher application context.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Qualifier
+public @interface ApplicationContext {
+}
diff --git a/src/com/android/launcher3/dagger/LauncherAppSingleton.java b/src/com/android/launcher3/dagger/LauncherAppSingleton.java
new file mode 100644
index 0000000..92c00b6
--- /dev/null
+++ b/src/com/android/launcher3/dagger/LauncherAppSingleton.java
@@ -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.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Scope;
+
+/**
+ * Scope annotation for singleton items within the LauncherAppComponent.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Scope
+public @interface LauncherAppSingleton {
+}
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
new file mode 100644
index 0000000..fb486f7
--- /dev/null
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.dagger;
+
+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;
+
+/**
+ * Launcher base component for Dagger injection.
+ *
+ * This class is not actually annotated as a Dagger component, since it is not used directly as one.
+ * Doing so generates unnecessary code bloat.
+ *
+ * See {@link LauncherAppComponent} for the one actually used by AOSP.
+ */
+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);
+        LauncherBaseAppComponent build();
+    }
+}
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
index 650df5a..52b454f 100644
--- a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
+++ b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
@@ -17,6 +17,7 @@
 package com.android.launcher3.debug
 
 import android.content.Context
+import android.util.Log
 import com.android.launcher3.util.MainThreadInitializedObject
 import com.android.launcher3.util.SafeCloseable
 
@@ -26,6 +27,8 @@
     WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
     RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
     WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
+    SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
+    SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED"),
 }
 
 /** Interface to create TestEventEmitters. */
@@ -50,5 +53,7 @@
 
     override fun close() {}
 
-    override fun sendEvent(event: TestEvent) {}
+    override fun sendEvent(event: TestEvent) {
+        Log.d("TestEventsEmitterProduction", "Event sent ${event.event}")
+    }
 }
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 db693f0..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 {
@@ -157,7 +157,8 @@
                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
                                 isEventOverAccessibleDropTargetBar(ev);
                         if (!isOverFolderOrSearchBar) {
-                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            sendTapOutsideFolderAccessibilityEvent(
+                                    currentFolder.getIsEditingName());
                             mHoverPointClosesFolder = true;
                             return true;
                         }
@@ -167,7 +168,8 @@
                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
                                 isEventOverAccessibleDropTargetBar(ev);
                         if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
-                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            sendTapOutsideFolderAccessibilityEvent(
+                                    currentFolder.getIsEditingName());
                             mHoverPointClosesFolder = true;
                             return true;
                         } else if (!isOverFolderOrSearchBar) {
@@ -195,7 +197,7 @@
 
 
     private boolean isInAccessibleDrag() {
-        return mActivity.getAccessibilityDelegate().isInAccessibleDrag();
+        return mContainer.getAccessibilityDelegate().isInAccessibleDrag();
     }
 
     @Override
@@ -208,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);
@@ -418,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/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 d3c1a02..5defef3 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
 import static com.android.launcher3.testing.shared.TestProtocol.FOLDER_OPENED_MESSAGE;
@@ -135,7 +136,8 @@
      * We avoid measuring {@link #mContent} with a 0 width or height, as this
      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
      */
-    private static final int MIN_CONTENT_DIMEN = 5;
+    @VisibleForTesting
+    static final int MIN_CONTENT_DIMEN = 5;
 
     public static final int STATE_CLOSED = 0;
     public static final int STATE_ANIMATING = 1;
@@ -143,7 +145,8 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({STATE_CLOSED, STATE_ANIMATING, STATE_OPEN})
-    public @interface FolderState {}
+    public @interface FolderState {
+    }
 
     /**
      * Time for which the scroll hint is shown before automatically changing page.
@@ -164,7 +167,7 @@
     private static final int FOLDER_COLOR_ANIMATION_DURATION = 200;
 
     private static final int REORDER_DELAY = 250;
-    private static final int ON_EXIT_CLOSE_DELAY = 400;
+    static final int ON_EXIT_CLOSE_DELAY = 400;
     private static final Rect sTempRect = new Rect();
     private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
 
@@ -184,10 +187,10 @@
                 || itemType == ITEM_TYPE_APP_PAIR;
     }
 
-    private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
-    private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
-    private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
-    final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
 
     final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
 
@@ -197,7 +200,7 @@
     // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
     // Anything specific to Launcher should use mLauncherDelegate, otherwise should
     // use mActivityContext.
-    protected final LauncherDelegate mLauncherDelegate;
+    protected LauncherDelegate mLauncherDelegate;
     protected final ActivityContext mActivityContext;
 
     protected DragController mDragController;
@@ -210,7 +213,7 @@
 
     @Thunk
     FolderPagedView mContent;
-    public FolderNameEditText mFolderName;
+    private FolderNameEditText mFolderName;
     private PageIndicatorDots mPageIndicator;
 
     protected View mFooter;
@@ -234,10 +237,10 @@
     private OnFolderStateChangedListener mPriorityOnFolderStateChangedListener;
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mRearrangeOnClose = false;
-    boolean mItemsInvalidated = false;
+    private boolean mItemsInvalidated = false;
     private View mCurrentDragView;
     private boolean mIsExternalDrag;
-    private boolean mDragInProgress = false;
+    private boolean mIsDragInProgress = false;
     private boolean mDeleteFolderOnDropCompleted = false;
     private boolean mSuppressFolderDeletion = false;
     private boolean mItemAddedBackToSelfViaIcon = false;
@@ -250,7 +253,7 @@
     private int mScrollAreaOffset;
 
     @Thunk
-    int mScrollHintDir = SCROLL_NONE;
+    private int mScrollHintDir = SCROLL_NONE;
     @Thunk
     int mCurrentScrollDir = SCROLL_NONE;
 
@@ -314,10 +317,7 @@
                 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
                 | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
         mFolderName.forceDisableSuggestions(true);
-        mFolderName.setPadding(mFolderName.getPaddingLeft(),
-                (mFooterHeight - mFolderName.getLineHeight()) / 2,
-                mFolderName.getPaddingRight(),
-                (mFooterHeight - mFolderName.getLineHeight()) / 2);
+
 
         mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
         setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
@@ -325,42 +325,54 @@
 
     public boolean onLongClick(View v) {
         // Return if global dragging is not enabled
-        if (!mLauncherDelegate.isDraggingEnabled()) return true;
+        if (!getIsLauncherDraggingEnabled()) return true;
         return startDrag(v, new DragOptions());
     }
 
+    @VisibleForTesting
+    boolean getIsLauncherDraggingEnabled() {
+        return mLauncherDelegate.isDraggingEnabled();
+    }
+
     public boolean startDrag(View v, DragOptions options) {
         Object tag = v.getTag();
         if (tag instanceof ItemInfo item) {
             mEmptyCellRank = item.rank;
             mCurrentDragView = v;
 
-            mDragController.addDragListener(this);
-            if (options.isAccessibleDrag) {
-                mDragController.addDragListener(new AccessibleDragListenerAdapter(
-                        mContent, FolderAccessibilityHelper::new) {
-                    @Override
-                    protected void enableAccessibleDrag(boolean enable,
-                            @Nullable DragObject dragObject) {
-                        super.enableAccessibleDrag(enable, dragObject);
-                        mFooter.setImportantForAccessibility(enable
-                                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-                    }
-                });
-            }
-
-            mLauncherDelegate.beginDragShared(v, this, options);
+            addDragListener(options);
+            callBeginDragShared(v, options);
         }
         return true;
     }
 
+    void callBeginDragShared(View v, DragOptions options) {
+        mLauncherDelegate.beginDragShared(v, this, options);
+    }
+
+    void addDragListener(DragOptions options) {
+        getDragController().addDragListener(this);
+        if (!options.isAccessibleDrag) {
+            return;
+        }
+        getDragController().addDragListener(new AccessibleDragListenerAdapter(
+                mContent, FolderAccessibilityHelper::new) {
+            @Override
+            protected void enableAccessibleDrag(boolean enable,
+                    @Nullable DragObject dragObject) {
+                super.enableAccessibleDrag(enable, dragObject);
+                mFooter.setImportantForAccessibility(enable
+                        ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                        : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            }
+        });
+    }
+
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
         if (dragObject.dragSource != this) {
             return;
         }
-
         mContent.removeItem(mCurrentDragView);
         mItemsInvalidated = true;
 
@@ -369,29 +381,23 @@
         try (SuppressInfoChanges s = new SuppressInfoChanges()) {
             mInfo.remove(dragObject.dragInfo, true);
         }
-        mDragInProgress = true;
+        mIsDragInProgress = true;
         mItemAddedBackToSelfViaIcon = false;
     }
 
     @Override
     public void onDragEnd() {
-        if (mIsExternalDrag && mDragInProgress) {
+        if (mIsExternalDrag && mIsDragInProgress) {
             completeDragExit();
         }
-        mDragInProgress = false;
-        mDragController.removeDragListener(this);
-    }
-
-    public boolean isEditingName() {
-        return mIsEditingName;
+        mIsDragInProgress = false;
+        getDragController().removeDragListener(this);
     }
 
     public void startEditingFolderName() {
-        post(() -> {
-            showLabelSuggestions();
-            mFolderName.setHint("");
-            mIsEditingName = true;
-        });
+        showLabelSuggestions();
+        mFolderName.setHint("");
+        mIsEditingName = true;
     }
 
     @Override
@@ -459,7 +465,11 @@
         return mFolderIcon;
     }
 
-    public void setDragController(DragController dragController) {
+    DragController getDragController() {
+        return mDragController;
+    }
+
+    void setDragController(DragController dragController) {
         mDragController = dragController;
     }
 
@@ -540,7 +550,7 @@
      * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
      * rest of the suggestions to InputMethodManager.
      */
-    private void showLabelSuggestions() {
+    void showLabelSuggestions() {
         if (mInfo.suggestedFolderNames == null) {
             return;
         }
@@ -634,11 +644,11 @@
      */
     public void beginExternalDrag() {
         mIsExternalDrag = true;
-        mDragInProgress = true;
+        mIsDragInProgress = true;
 
         // Since this folder opened by another controller, it might not get onDrop or
         // onDropComplete. Perform cleanup once drag-n-drop ends.
-        mDragController.addDragListener(this);
+        getDragController().addDragListener(this);
 
         ArrayList<ItemInfo> items = new ArrayList<>(mInfo.getContents());
         mEmptyCellRank = items.size();
@@ -662,16 +672,12 @@
      * is played.
      */
     private void animateOpen(List<ItemInfo> items, int pageNo) {
-        if (items == null || items.size() <= 1) {
-            Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+        if (!shouldAnimateOpen(items)) {
             return;
         }
 
         Folder openFolder = getOpen(mActivityContext);
-        if (openFolder != null && openFolder != this) {
-            // Close any open folder before opening a folder.
-            openFolder.close(true);
-        }
+        closeOpenFolder(openFolder);
 
         mContent.bindItems(items);
         centerAboutIcon();
@@ -685,7 +691,7 @@
         // There was a one-off crash where the folder had a parent already.
         if (getParent() == null) {
             dragLayer.addView(this);
-            mDragController.addDropTarget(this);
+            getDragController().addDropTarget(this);
         } else {
             if (FeatureFlags.IS_STUDIO_BUILD) {
                 Log.e(TAG, "Opening folder (" + this + ") which already has a parent:"
@@ -734,7 +740,7 @@
 
             // Do not update the flag if we are in drag mode. The flag will be updated, when we
             // actually drop the icon.
-            final boolean updateAnimationFlag = !mDragInProgress;
+            final boolean updateAnimationFlag = !mIsDragInProgress;
             anim.addListener(new AnimatorListenerAdapter() {
 
                 @SuppressLint("InlinedApi")
@@ -764,16 +770,41 @@
         addAnimationStartListeners(anim);
         // Because t=0 has the folder match the folder icon, we can skip the
         // first frame and have the same movement one frame earlier.
+        Log.d("b/311077782", "Folder.animateOpen");
         anim.setCurrentPlayTime(Math.min(getSingleFrameMs(getContext()), anim.getTotalDuration()));
         anim.start();
 
         // Make sure the folder picks up the last drag move even if the finger doesn't move.
-        if (mDragController.isDragging()) {
-            mDragController.forceTouchMove();
+        if (getDragController().isDragging()) {
+            getDragController().forceTouchMove();
         }
         mContent.verifyVisibleHighResIcons(mContent.getNextPage());
     }
 
+    /**
+     * Determines whether we should animate the folder opening.
+     */
+    boolean shouldAnimateOpen(List<ItemInfo> items) {
+        if (items == null || items.size() <= 1) {
+            Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * If there's a folder already open, we want to close it before opening another one.
+     */
+    @VisibleForTesting
+    boolean closeOpenFolder(Folder openFolder) {
+        if (openFolder != null && openFolder != this) {
+            // Close any open folder before opening a folder.
+            openFolder.close(true);
+            return true;
+        }
+        return false;
+    }
+
     @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_FOLDER) != 0;
@@ -787,7 +818,7 @@
             mCurrentAnimator.cancel();
         }
 
-        if (isEditingName()) {
+        if (mIsEditingName) {
             mFolderName.dispatchBackKey();
         }
 
@@ -871,7 +902,7 @@
         if (parent != null) {
             parent.removeView(this);
         }
-        mDragController.removeDropTarget(this);
+        getDragController().removeDropTarget(this);
         clearFocus();
         if (mFolderIcon != null) {
             mFolderIcon.setVisibility(View.VISIBLE);
@@ -892,12 +923,12 @@
             mRearrangeOnClose = false;
         }
         if (getItemCount() <= 1) {
-            if (!mDragInProgress && !mSuppressFolderDeletion) {
+            if (!mIsDragInProgress && !mSuppressFolderDeletion) {
                 replaceFolderWithFinalItem();
-            } else if (mDragInProgress) {
+            } else if (mIsDragInProgress) {
                 mDeleteFolderOnDropCompleted = true;
             }
-        } else if (!mDragInProgress) {
+        } else if (!mIsDragInProgress) {
             mContent.unbindItems();
         }
         mSuppressFolderDeletion = false;
@@ -1017,7 +1048,8 @@
         }
     }
 
-    private void clearDragInfo() {
+    @VisibleForTesting
+    void clearDragInfo() {
         mCurrentDragView = null;
         mIsExternalDrag = false;
     }
@@ -1058,7 +1090,8 @@
             if (getItemCount() <= 1) {
                 mDeleteFolderOnDropCompleted = true;
             }
-            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
+            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon
+                    && target != this) {
                 replaceFolderWithFinalItem();
             }
         } else {
@@ -1089,7 +1122,7 @@
         }
 
         mDeleteFolderOnDropCompleted = false;
-        mDragInProgress = false;
+        mIsDragInProgress = false;
         mItemAddedBackToSelfViaIcon = false;
         mCurrentDragView = null;
 
@@ -1106,7 +1139,7 @@
     }
 
     private void updateItemLocationsInDatabaseBatch(boolean isBind) {
-        FolderGridOrganizer verifier = new FolderGridOrganizer(
+        FolderGridOrganizer verifier = createFolderGridOrganizer(
                 mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
 
         ArrayList<ItemInfo> items = new ArrayList<>();
@@ -1132,7 +1165,7 @@
     }
 
     public void notifyDrop() {
-        if (mDragInProgress) {
+        if (mIsDragInProgress) {
             mItemAddedBackToSelfViaIcon = true;
         }
     }
@@ -1175,28 +1208,41 @@
     }
 
     protected int getContentAreaHeight() {
-        DeviceProfile grid = mActivityContext.getDeviceProfile();
-        int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
-                - mFooterHeight;
-        int height = Math.min(maxContentAreaHeight,
+        int height = Math.min(getMaxContentAreaHeight(),
                 mContent.getDesiredHeight());
         return Math.max(height, MIN_CONTENT_DIMEN);
     }
 
-    private int getContentAreaWidth() {
+    @VisibleForTesting
+    int getMaxContentAreaHeight() {
+        DeviceProfile grid = mActivityContext.getDeviceProfile();
+        return grid.availableHeightPx - grid.getTotalWorkspacePadding().y
+                - getFooterHeight();
+    }
+
+    @VisibleForTesting
+    int getContentAreaWidth() {
         return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
     }
 
-    private int getFolderWidth() {
+    @VisibleForTesting
+    int getFolderWidth() {
         return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
     }
 
-    private int getFolderHeight() {
+    @VisibleForTesting
+    int getFolderHeight() {
         return getFolderHeight(getContentAreaHeight());
     }
 
-    private int getFolderHeight(int contentAreaHeight) {
-        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
+    @VisibleForTesting
+    int getFolderHeight(int contentAreaHeight) {
+        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + getFooterHeight();
+    }
+
+    @VisibleForTesting
+    int getFooterHeight() {
+        return mFooterHeight;
     }
 
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -1366,7 +1412,7 @@
         }
 
         // Clear the drag info, as it is no longer being dragged.
-        mDragInProgress = false;
+        mIsDragInProgress = false;
 
         if (mContent.getPageCount() > 1) {
             // The animation has already been shown while opening the folder.
@@ -1404,7 +1450,7 @@
 
     @Override
     public void onAdd(ItemInfo item, int rank) {
-        FolderGridOrganizer verifier = new FolderGridOrganizer(
+        FolderGridOrganizer verifier = createFolderGridOrganizer(
                 mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
         verifier.updateRankAndPos(item, rank);
         mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
@@ -1435,7 +1481,8 @@
         }
     }
 
-    private View getViewForInfo(final ItemInfo item) {
+    @VisibleForTesting
+    View getViewForInfo(final ItemInfo item) {
         return mContent.iterateOverItems((info, view) -> info == item);
     }
 
@@ -1493,7 +1540,7 @@
             if (hasFocus) {
                 mFromLabelState = mInfo.getFromLabelState();
                 mFromTitle = mInfo.title;
-                startEditingFolderName();
+                post(this::startEditingFolderName);
             } else {
                 StatsLogger statsLogger = mStatsLogManager.logger()
                         .withItemInfo(mInfo)
@@ -1626,7 +1673,7 @@
     /** Navigation bar back key or hardware input back key has been issued. */
     @Override
     public void onBackInvoked() {
-        if (isEditingName()) {
+        if (mIsEditingName) {
             mFolderName.dispatchBackKey();
         } else {
             super.onBackInvoked();
@@ -1638,7 +1685,7 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             BaseDragLayer dl = (BaseDragLayer) getParent();
 
-            if (isEditingName()) {
+            if (mIsEditingName) {
                 if (!dl.isEventOverView(mFolderName, ev)) {
                     mFolderName.dispatchBackKey();
                     return true;
@@ -1685,6 +1732,95 @@
         return mContent;
     }
 
+    @VisibleForTesting
+    void setItemAddedBackToSelfViaIcon(boolean value) {
+        mItemAddedBackToSelfViaIcon = value;
+    }
+
+    @VisibleForTesting
+    boolean getItemAddedBackToSelfViaIcon() {
+        return mItemAddedBackToSelfViaIcon;
+    }
+
+    @VisibleForTesting
+    void setIsDragInProgress(boolean value) {
+        mIsDragInProgress = value;
+    }
+
+    @VisibleForTesting
+    boolean getIsDragInProgress() {
+        return mIsDragInProgress;
+    }
+
+    @VisibleForTesting
+    View getCurrentDragView() {
+        return mCurrentDragView;
+    }
+
+    @VisibleForTesting
+    void setCurrentDragView(View view) {
+        mCurrentDragView = view;
+    }
+
+    @VisibleForTesting
+    boolean getItemsInvalidated() {
+        return mItemsInvalidated;
+    }
+
+    @VisibleForTesting
+    void setItemsInvalidated(boolean value) {
+        mItemsInvalidated = value;
+    }
+
+    @VisibleForTesting
+    boolean getIsExternalDrag() {
+        return mIsExternalDrag;
+    }
+
+    @VisibleForTesting
+    void setIsExternalDrag(boolean value) {
+        mIsExternalDrag = value;
+    }
+
+    public boolean getIsEditingName() {
+        return mIsEditingName;
+    }
+
+    @VisibleForTesting
+    void setIsEditingName(boolean value) {
+        mIsEditingName = value;
+    }
+
+    @VisibleForTesting
+    void setFolderName(FolderNameEditText value) {
+        mFolderName = value;
+    }
+
+    @VisibleForTesting
+    FolderNameEditText getFolderName() {
+        return mFolderName;
+    }
+
+    @VisibleForTesting
+    boolean getIsOpen() {
+        return mIsOpen;
+    }
+
+    @VisibleForTesting
+    void setIsOpen(boolean value) {
+        mIsOpen = value;
+    }
+
+    @VisibleForTesting
+    boolean getRearrangeOnClose() {
+        return mRearrangeOnClose;
+    }
+
+    @VisibleForTesting
+    void setRearrangeOnClose(boolean value) {
+        mRearrangeOnClose = value;
+    }
+
     /** Returns the height of the current folder's bottom edge from the bottom of the screen. */
     private int getHeightFromBottom() {
         BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
@@ -1695,10 +1831,15 @@
     }
 
     @VisibleForTesting
-    public boolean getDeleteFolderOnDropCompleted() {
+    boolean getDeleteFolderOnDropCompleted() {
         return mDeleteFolderOnDropCompleted;
     }
 
+    @VisibleForTesting
+    void setDeleteFolderOnDropCompleted(boolean value) {
+        mDeleteFolderOnDropCompleted = value;
+    }
+
     /**
      * Save this listener for the special case of when we update the state and concurrently
      * add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
@@ -1708,7 +1849,13 @@
         mPriorityOnFolderStateChangedListener = listener;
     }
 
-    private void setState(@FolderState int newState) {
+    @VisibleForTesting
+    int getState() {
+        return mState;
+    }
+
+    @VisibleForTesting
+    void setState(@FolderState int newState) {
         mState = newState;
         if (mPriorityOnFolderStateChangedListener != null) {
             mPriorityOnFolderStateChangedListener.onFolderStateChanged(mState);
@@ -1720,6 +1867,60 @@
         }
     }
 
+    @VisibleForTesting
+    Alarm getOnExitAlarm() {
+        return mOnExitAlarm;
+    }
+
+    @VisibleForTesting
+    void setOnExitAlarm(Alarm value) {
+        mOnExitAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getReorderAlarm() {
+        return mReorderAlarm;
+    }
+
+    @VisibleForTesting
+    void setReorderAlarm(Alarm value) {
+        mReorderAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getOnScrollHintAlarm() {
+        return mOnScrollHintAlarm;
+    }
+
+    @VisibleForTesting
+    void setOnScrollHintAlarm(Alarm value) {
+        mOnScrollHintAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getScrollPauseAlarm() {
+        return mScrollPauseAlarm;
+    }
+
+    @VisibleForTesting
+    void setScrollPauseAlarm(Alarm value) {
+        mScrollPauseAlarm = value;
+    }
+
+    @VisibleForTesting
+    int getScrollHintDir() {
+        return mScrollHintDir;
+    }
+
+    @VisibleForTesting
+    void setScrollHintDir(int value) {
+        mScrollHintDir = value;
+    }
+
+    @VisibleForTesting
+    int getScrollAreaOffset() {
+        return mScrollAreaOffset;
+    }
     /**
      * Adds the provided listener to the running list of Folder listeners
      * {@link #mOnFolderStateChangedListeners}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 37a8d9b..588a6db 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -99,7 +100,7 @@
 
         mContext = folder.getContext();
         mDeviceProfile = folder.mActivityContext.getDeviceProfile();
-        mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile);
+        mPreviewVerifier = createFolderGridOrganizer(mDeviceProfile);
 
         mIsOpening = isOpening;
 
@@ -255,8 +256,8 @@
                 mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
 
         // Fade in the folder name, as the text can overlap the icons when grid size is small.
-        mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
-        play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
+        mFolder.getFolderName().setAlpha(mIsOpening ? 0f : 1f);
+        play(a, getAnimator(mFolder.getFolderName(), View.ALPHA, 0, 1),
                 mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
                 mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
 
@@ -317,7 +318,7 @@
                 mFolder.mFooter.setScaleX(1f);
                 mFolder.mFooter.setScaleY(1f);
                 mFolder.mFooter.setTranslationX(0f);
-                mFolder.mFolderName.setAlpha(1f);
+                mFolder.getFolderName().setAlpha(1f);
 
                 mFolder.setClipChildren(mFolderClipChildren);
                 mFolder.setClipToPadding(mFolderClipToPadding);
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index 593673d..a7ab7b9 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -47,13 +47,20 @@
     /**
      * Note: must call {@link #setFolderInfo(FolderInfo)} manually for verifier to work.
      */
-    public FolderGridOrganizer(DeviceProfile profile) {
-        mMaxCountX = profile.numFolderColumns;
-        mMaxCountY = profile.numFolderRows;
+    public FolderGridOrganizer(int maxCountX, int maxCountY) {
+        mMaxCountX = maxCountX;
+        mMaxCountY = maxCountY;
         mMaxItemsPerPage = mMaxCountX * mMaxCountY;
     }
 
     /**
+     * Creates a FolderGridOrganizer for the given DeviceProfile
+     */
+    public static FolderGridOrganizer createFolderGridOrganizer(DeviceProfile profile) {
+        return new FolderGridOrganizer(profile.numFolderColumns, profile.numFolderRows);
+    }
+
+    /**
      * Updates the organizer with the provided folder info
      */
     public FolderGridOrganizer setFolderInfo(FolderInfo info) {
@@ -194,6 +201,7 @@
             int row = rank / mCountX;
             return col < PREVIEW_MAX_COLUMNS && row < PREVIEW_MAX_ROWS;
         }
+        // If we have less than 4 items do this
         return rank < MAX_NUM_ITEMS_IN_PREVIEW;
     }
 }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 00636a3..de1bcc3 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY;
@@ -82,7 +83,7 @@
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.views.IconLabelDotView;
+import com.android.launcher3.views.FloatingIconViewCompanion;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
 import java.util.ArrayList;
@@ -92,7 +93,7 @@
 /**
  * An icon that can appear on in the workspace representing an {@link Folder}.
  */
-public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
+public class FolderIcon extends FrameLayout implements FolderListener, FloatingIconViewCompanion,
         DraggableView, Reorderable {
 
     private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
@@ -223,7 +224,7 @@
 
         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
 
-        icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile());
+        icon.mPreviewVerifier = createFolderGridOrganizer(activity.getDeviceProfile());
         icon.mPreviewVerifier.setFolderInfo(folderInfo);
         icon.updatePreviewItems(false);
 
@@ -458,7 +459,7 @@
 
         mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
         onTitleChanged(mInfo.title);
-        mFolder.mFolderName.setText(mInfo.title);
+        mFolder.getFolderName().setText(mInfo.title);
 
         // Logging for folder creation flow
         StatsLogManager.newInstance(getContext()).logger()
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 8eaa0dc..fe26194 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -100,10 +101,21 @@
     private boolean mViewsBound = false;
 
     public FolderPagedView(Context context, AttributeSet attrs) {
+        this(
+                context,
+                attrs,
+                createFolderGridOrganizer(ActivityContext.lookupContext(context).getDeviceProfile())
+        );
+    }
+
+    public FolderPagedView(
+            Context context,
+            AttributeSet attrs,
+            FolderGridOrganizer folderGridOrganizer
+    ) {
         super(context, attrs);
         ActivityContext activityContext = ActivityContext.lookupContext(context);
-        DeviceProfile profile = activityContext.getDeviceProfile();
-        mOrganizer = new FolderGridOrganizer(profile);
+        mOrganizer = folderGridOrganizer;
 
         mIsRtl = Utilities.isRtl(getResources());
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -361,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.mFolderName.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/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 6311638..2276ac7 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -41,14 +42,13 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.apppairs.AppPairIconDrawingParams;
 import com.android.launcher3.apppairs.AppPairIconGraphic;
-import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
@@ -442,10 +442,8 @@
     @VisibleForTesting
     public void setDrawable(PreviewItemDrawingParams p, ItemInfo item) {
         if (item instanceof WorkspaceItemInfo wii) {
-            if (wii.hasPromiseIconUi() || (wii.runtimeStatusFlags
-                    & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
-                PreloadIconDrawable drawable = newPendingIcon(mContext, wii);
-                p.drawable = drawable;
+            if (isActivePendingIcon(wii)) {
+                p.drawable = newPendingIcon(mContext, wii);
             } else {
                 p.drawable = wii.newIcon(mContext,
                         Themes.isThemedIconEnabled(mContext) ? FLAG_THEMED : 0);
@@ -463,4 +461,14 @@
         // callback will be released when the folder is opened.
         p.drawable.setCallback(mIcon);
     }
+
+    /**
+     * Returns true if item is a Promise Icon or actively downloading, and the item is not an
+     * inactive archived app.
+     */
+    private boolean isActivePendingIcon(WorkspaceItemInfo item) {
+        return (item.hasPromiseIconUi()
+                || (item.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0)
+                && !(Flags.useNewIconForArchivedApps() && item.isInactiveArchive());
+    }
 }
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index dc8694d..7367f2e 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -16,11 +16,13 @@
 package com.android.launcher3.graphics;
 
 import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
+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.Themes.isThemedIconEnabled;
 
 import android.content.ContentProvider;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -28,46 +30,76 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.Message;
 import android.os.Messenger;
-import android.util.ArrayMap;
+import android.text.TextUtils;
 import android.util.Log;
-import android.util.Pair;
+
+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;
 
 /**
  * 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";
@@ -80,14 +112,16 @@
     private static final String KEY_SURFACE_PACKAGE = "surface_package";
     private static final String KEY_CALLBACK = "callback";
     public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row";
+    public static final String KEY_GRID_NAME = "grid_name";
 
     private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337;
+    private static final int MESSAGE_ID_UPDATE_SHAPE = 2586;
+    private static final int MESSAGE_ID_UPDATE_GRID = 7414;
+    private static final int MESSAGE_ID_UPDATE_COLOR = 856;
 
-    /**
-     * Here we use the IBinder and the screen ID as the key of the active previews.
-     */
-    private final ArrayMap<Pair<IBinder, Integer>, PreviewLifecycleObserver> mActivePreviews =
-            new ArrayMap<>();
+    // Set of all active previews used to track duplicate memory allocations
+    private final Set<PreviewLifecycleObserver> mActivePreviews =
+            Collections.newSetFromMap(new WeakHashMap<>());
 
     @Override
     public boolean onCreate() {
@@ -97,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)
@@ -141,14 +203,28 @@
 
     @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        switch (uri.getPath()) {
+        String path = uri.getPath();
+        Context context = getContext();
+        if (path == null || context == null) {
+            return 0;
+        }
+        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(getContext());
+                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
                 // Verify that this is a valid grid option
                 GridOption match = null;
-                for (GridOption option : idp.parseAllGridOptions(getContext())) {
-                    if (option.name.equals(gridName)) {
+                for (GridOption option : idp.parseAllGridOptions(context)) {
+                    String name = option.name;
+                    if (name != null && name.equals(gridName)) {
                         match = option;
                         break;
                     }
@@ -157,15 +233,23 @@
                     return 0;
                 }
 
-                idp.setCurrentGrid(getContext(), gridName);
-                getContext().getContentResolver().notifyChange(uri, null);
+                idp.setCurrentGrid(context, gridName);
+                if (Flags.newCustomizationPickerUi()) {
+                    try {
+                        // Wait for device profile to be fully reloaded and applied to the launcher
+                        loadModelSync(context);
+                    } catch (ExecutionException | InterruptedException e) {
+                        Log.e(TAG, "Fail to load model", e);
+                    }
+                }
+                context.getContentResolver().notifyChange(uri, null);
                 return 1;
             }
             case ICON_THEMED:
             case SET_ICON_THEMED: {
-                LauncherPrefs.get(getContext())
+                LauncherPrefs.get(context)
                         .put(THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE));
-                getContext().getContentResolver().notifyChange(uri, null);
+                context.getContentResolver().notifyChange(uri, null);
                 return 1;
             }
             default:
@@ -173,31 +257,61 @@
         }
     }
 
+    /**
+     * Loads the model in memory synchronously
+     */
+    private void loadModelSync(Context context) throws ExecutionException, InterruptedException {
+        Preconditions.assertNonUiThread();
+        BgDataModel.Callbacks emptyCallbacks = new BgDataModel.Callbacks() { };
+        LauncherModel launcherModel = LauncherAppState.getInstance(context).getModel();
+        MAIN_EXECUTOR.submit(
+                () -> launcherModel.addCallbacksAndLoad(emptyCallbacks)
+        ).get();
+
+        Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+        MAIN_EXECUTOR.submit(
+                () -> launcherModel.removeCallbacks(emptyCallbacks)
+        ).get();
+    }
+
     @Override
-    public Bundle call(String method, String arg, Bundle extras) {
-        if (getContext().checkPermission("android.permission.BIND_WALLPAPER",
+    public Bundle call(@NonNull String method, String arg, Bundle extras) {
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+
+        if (context.checkPermission("android.permission.BIND_WALLPAPER",
                 Binder.getCallingPid(), Binder.getCallingUid())
                 != PackageManager.PERMISSION_GRANTED) {
             return null;
         }
 
-        if (!METHOD_GET_PREVIEW.equals(method)) {
+        if (METHOD_GET_PREVIEW.equals(method)) {
+            return getPreview(extras);
+        } else {
             return null;
         }
-        return getPreview(extras);
     }
 
     private synchronized Bundle getPreview(Bundle request) {
-        PreviewLifecycleObserver observer = null;
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+        RunnableList lifeCycleTracker = new RunnableList();
         try {
-            PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(getContext(), request);
+            PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
+                    getContext(), lifeCycleTracker, request);
+            PreviewLifecycleObserver observer =
+                    new PreviewLifecycleObserver(lifeCycleTracker, renderer);
 
-            observer = new PreviewLifecycleObserver(renderer);
-            // Destroy previous
-            destroyObserver(mActivePreviews.get(observer.getIdentifier()));
-            mActivePreviews.put(observer.getIdentifier(), observer);
+            // Destroy previous renderers to avoid any duplicate memory
+            mActivePreviews.stream().filter(observer::isSameRenderer).forEach(o ->
+                    MAIN_EXECUTOR.execute(o.lifeCycleTracker::executeAllAndDestroy));
 
             renderer.loadAsync();
+            lifeCycleTracker.add(() -> renderer.getHostToken().unlinkToDeath(observer, 0));
             renderer.getHostToken().linkToDeath(observer, 0);
 
             Bundle result = new Bundle();
@@ -211,33 +325,23 @@
             return result;
         } catch (Exception e) {
             Log.e(TAG, "Unable to generate preview", e);
-            if (observer != null) {
-                destroyObserver(observer);
-            }
+            MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
             return null;
         }
     }
 
-    private synchronized void destroyObserver(PreviewLifecycleObserver observer) {
-        if (observer == null || observer.destroyed) {
-            return;
-        }
-        observer.destroyed = true;
-        observer.renderer.getHostToken().unlinkToDeath(observer, 0);
-        Executors.MAIN_EXECUTOR.execute(observer.renderer::destroy);
-        PreviewLifecycleObserver cached = mActivePreviews.get(observer.getIdentifier());
-        if (cached == observer) {
-            mActivePreviews.remove(observer.getIdentifier());
-        }
-    }
+    private static class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient {
 
-    private class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient {
-
+        public final RunnableList lifeCycleTracker;
         public final PreviewSurfaceRenderer renderer;
         public boolean destroyed = false;
 
-        PreviewLifecycleObserver(PreviewSurfaceRenderer renderer) {
+        PreviewLifecycleObserver(
+                RunnableList lifeCycleTracker,
+                PreviewSurfaceRenderer renderer) {
+            this.lifeCycleTracker = lifeCycleTracker;
             this.renderer = renderer;
+            lifeCycleTracker.add(() -> destroyed = true);
         }
 
         @Override
@@ -245,26 +349,55 @@
             if (destroyed) {
                 return true;
             }
-            if (message.what == MESSAGE_ID_UPDATE_PREVIEW) {
-                renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
-            } else {
-                destroyObserver(this);
+
+            switch (message.what) {
+                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");
+                    MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
+                    break;
             }
+
             return true;
         }
 
         @Override
         public void binderDied() {
-            destroyObserver(this);
+            MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
         }
 
         /**
-         * Returns a key that should make the PreviewSurfaceRenderer unique and if two of them have
-         * the same key they will be treated as the same PreviewSurfaceRenderer. Primary this is
-         * used to prevent memory leaks by removing the old PreviewSurfaceRenderer.
+         * Two renderers are considered same if they have the same host token and display Id
          */
-        public Pair<IBinder, Integer> getIdentifier() {
-            return new Pair<>(renderer.getHostToken(), renderer.getDisplayId());
+        public boolean isSameRenderer(PreviewLifecycleObserver plo) {
+            return plo != null
+                    && plo.renderer.getHostToken().equals(renderer.getHostToken())
+                    && plo.renderer.getDisplayId() == renderer.getDisplayId();
         }
     }
 }
diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java
index 778b32a..00f1c67 100644
--- a/src/com/android/launcher3/graphics/IconPalette.java
+++ b/src/com/android/launcher3/graphics/IconPalette.java
@@ -16,22 +16,15 @@
 
 package com.android.launcher3.graphics;
 
-import android.app.Notification;
 import android.content.Context;
 import android.graphics.Color;
-import android.util.Log;
 
-import androidx.core.graphics.ColorUtils;
-
-import com.android.launcher3.R;
 import com.android.launcher3.util.Themes;
 
 /**
  * Contains colors based on the dominant color of an icon.
  */
 public class IconPalette {
-
-    private static final boolean DEBUG = false;
     private static final String TAG = "IconPalette";
 
     private static final float MIN_PRELOAD_COLOR_SATURATION = 0.2f;
@@ -54,95 +47,4 @@
         }
         return result;
     }
-
-    /**
-     * Resolves a color such that it has enough contrast to be used as the
-     * color of an icon or text on the given background color.
-     *
-     * @return a color of the same hue with enough contrast against the background.
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    public static int resolveContrastColor(Context context, int color, int background) {
-        final int resolvedColor = resolveColor(context, color);
-
-        int contrastingColor = ensureTextContrast(resolvedColor, background);
-
-        if (contrastingColor != resolvedColor) {
-            if (DEBUG){
-                Log.w(TAG, String.format(
-                        "Enhanced contrast of notification for %s " +
-                                "%s (over background) by changing #%s to %s",
-                        context.getPackageName(),
-                        contrastChange(resolvedColor, contrastingColor, background),
-                        Integer.toHexString(resolvedColor), Integer.toHexString(contrastingColor)));
-            }
-        }
-        return contrastingColor;
-    }
-
-    /**
-     * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    private static int resolveColor(Context context, int color) {
-        if (color == Notification.COLOR_DEFAULT) {
-            return context.getColor(R.color.notification_icon_default_color);
-        }
-        return color;
-    }
-
-    /** For debugging. This was copied from com.android.internal.util.NotificationColorUtil. */
-    private static String contrastChange(int colorOld, int colorNew, int bg) {
-        return String.format("from %.2f:1 to %.2f:1",
-                ColorUtils.calculateContrast(colorOld, bg),
-                ColorUtils.calculateContrast(colorNew, bg));
-    }
-
-    /**
-     * Finds a text color with sufficient contrast over bg that has the same hue as the original
-     * color.
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    private static int ensureTextContrast(int color, int bg) {
-        return findContrastColor(color, bg, 4.5);
-    }
-    /**
-     * Finds a suitable color such that there's enough contrast.
-     *
-     * @param fg the color to start searching from.
-     * @param bg the color to ensure contrast against.
-     * @param minRatio the minimum contrast ratio required.
-     * @return a color with the same hue as {@param color}, potentially darkened to meet the
-     *          contrast ratio.
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    private static int findContrastColor(int fg, int bg, double minRatio) {
-        if (ColorUtils.calculateContrast(fg, bg) >= minRatio) {
-            return fg;
-        }
-
-        double[] lab = new double[3];
-        ColorUtils.colorToLAB(bg, lab);
-        double bgL = lab[0];
-        ColorUtils.colorToLAB(fg, lab);
-        double fgL = lab[0];
-        boolean isBgDark = bgL < 50;
-
-        double low = isBgDark ? fgL : 0, high = isBgDark ? 100 : fgL;
-        final double a = lab[1], b = lab[2];
-        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
-            final double l = (low + high) / 2;
-            fg = ColorUtils.LABToColor(l, a, b);
-            if (ColorUtils.calculateContrast(fg, bg) > minRatio) {
-                if (isBgDark) high = l; else low = l;
-            } else {
-                if (isBgDark) low = l; else high = l;
-            }
-        }
-        return ColorUtils.LABToColor(low, a, b);
-    }
 }
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 6088941..f0e4fc4 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
 import static com.android.launcher3.BubbleTextView.DISPLAY_WORKSPACE;
 import static com.android.launcher3.DeviceProfile.DEFAULT_SCALE;
+import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_PREVIEW_RENDERER;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
@@ -68,7 +69,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.apppairs.AppPairIcon;
@@ -78,8 +78,6 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.FolderInfo;
@@ -100,12 +98,14 @@
 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;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
@@ -151,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());
@@ -207,14 +215,28 @@
             mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel);
         }
 
-        if (Utilities.ATLEAST_S) {
+        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;
-        } else {
-            mWallpaperColorResources = null;
+            mWallpaperColorResources = wallpaperColors != null
+                    ? LocalColorExtractor.newInstance(context).generateColorsOverride(
+                    wallpaperColors)
+                    : null;
         }
         mAppWidgetHost = new LauncherPreviewAppWidgetHost(context);
     }
@@ -321,12 +343,12 @@
         mUiHandler.post(() -> {
             if (mDp.isTaskbarPresent) {
                 // hotseat icons on bottom
-                mHotseat.setIconsAlpha(hide ? 0 : 1);
+                mHotseat.setIconsAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER);
                 if (mDp.isQsbInline) {
-                    mHotseat.setQsbAlpha(hide ? 0 : 1);
+                    mHotseat.setQsbAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER);
                 }
             } else {
-                mHotseat.setQsbAlpha(hide ? 0 : 1);
+                mHotseat.setQsbAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER);
             }
         });
     }
@@ -376,15 +398,6 @@
                 getApplicationContext(), providerInfo));
     }
 
-    private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
-        WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
-                info.providerName, info.user, mContext);
-        if (widgetItem == null) {
-            return;
-        }
-        inflateAndAddWidgets(info, widgetItem.widgetInfo);
-    }
-
     private void inflateAndAddWidgets(
             LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
         AppWidgetHostView view = mAppWidgetHost.createView(
@@ -468,17 +481,22 @@
                     break;
             }
         }
+        Map<ComponentKey, AppWidgetProviderInfo> widgetsMap = widgetProviderInfoMap;
         for (ItemInfo itemInfo : currentAppWidgets) {
             switch (itemInfo.itemType) {
                 case Favorites.ITEM_TYPE_APPWIDGET:
                 case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                    if (widgetProviderInfoMap != null) {
-                        inflateAndAddWidgets(
-                                (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap);
-                    } else {
-                        inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
-                                dataModel.widgetsModel);
+                    if (widgetsMap == null) {
+                        widgetsMap = dataModel.widgetsModel.getWidgetsByComponentKey()
+                                .entrySet()
+                                .stream()
+                                .filter(entry -> entry.getValue().widgetInfo != null)
+                                .collect(Collectors.toMap(
+                                        Map.Entry::getKey,
+                                        entry -> entry.getValue().widgetInfo
+                                ));
                     }
+                    inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo, widgetsMap);
                     break;
                 default:
                     break;
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index addd072..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,12 +35,14 @@
 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;
 import android.view.SurfaceControlViewHost.SurfacePackage;
 import android.view.View;
 import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -53,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;
@@ -61,6 +66,7 @@
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.widget.LocalColorExtractor;
+import com.android.systemui.shared.Flags;
 
 import java.util.ArrayList;
 import java.util.Map;
@@ -79,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;
@@ -89,22 +98,30 @@
     private final int mDisplayId;
     private final Display mDisplay;
     private final WallpaperColors mWallpaperColors;
-    private final RunnableList mOnDestroyCallbacks = new RunnableList();
+    private SparseIntArray mPreviewColorOverride;
+    @Nullable private Boolean mDarkMode;
+    private final RunnableList mLifeCycleTracker;
 
     private final SurfaceControlViewHost mSurfaceControlViewHost;
 
     private boolean mDestroyed = false;
     private LauncherPreviewRenderer mRenderer;
     private boolean mHideQsb;
+    @Nullable private FrameLayout mViewRoot = null;
 
-    public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
+    public PreviewSurfaceRenderer(
+            Context context, RunnableList lifecycleTracker, Bundle bundle) throws Exception {
         mContext = context;
+        mLifeCycleTracker = lifecycleTracker;
         mGridName = bundle.getString("name");
         bundle.remove("name");
         if (mGridName == null) {
             mGridName = InvariantDeviceProfile.getCurrentGridName(context);
         }
         mWallpaperColors = bundle.getParcelable(KEY_COLORS);
+        if (Flags.newCustomizationPickerUi()) {
+            updateColorOverrides(bundle);
+        }
         mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
 
         mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
@@ -117,11 +134,13 @@
             throw new IllegalArgumentException("Display ID does not match any displays.");
         }
 
-        mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() ->
-                new SurfaceControlViewHost(mContext, context.getSystemService(DisplayManager.class)
-                        .getDisplay(DEFAULT_DISPLAY), mHostToken)
-        ).get(5, TimeUnit.SECONDS);
-        mOnDestroyCallbacks.add(mSurfaceControlViewHost::release);
+        mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() -> new MySurfaceControlViewHost(
+                mContext,
+                context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
+                mHostToken,
+                mLifeCycleTracker))
+                .get(5, TimeUnit.SECONDS);
+        mLifeCycleTracker.add(this::destroy);
     }
 
     public int getDisplayId() {
@@ -136,25 +155,18 @@
         return mSurfaceControlViewHost.getSurfacePackage();
     }
 
-    /**
-     * Destroys the preview and all associated data
-     */
-    @UiThread
-    public void destroy() {
+    private void destroy() {
         mDestroyed = true;
-        mOnDestroyCallbacks.executeAllAndDestroy();
     }
 
     /**
      * A function that queries for the launcher app widget span info
      *
-     * @param context The context to get the content resolver from, should be related to launcher
      * @return A SparseArray with the app widget id being the key and the span info being the values
      */
     @WorkerThread
     @Nullable
-    public SparseArray<Size> getLoadedLauncherWidgetInfo(
-            @NonNull final Context context) {
+    public SparseArray<Size> getLoadedLauncherWidgetInfo() {
         final SparseArray<Size> widgetInfo = new SparseArray<>();
         final String query = LauncherSettings.Favorites.ITEM_TYPE + " = "
                 + LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
@@ -194,6 +206,19 @@
     }
 
     /**
+     * Update the grid of the launcher preview
+     *
+     * @param gridName Name of the grid, e.g. normal, practical
+     */
+    public void updateGrid(@NonNull String gridName) {
+        if (gridName.equals(mGridName)) {
+            return;
+        }
+        mGridName = gridName;
+        loadAsync();
+    }
+
+    /**
      * Hides the components in the bottom row.
      *
      * @param hide True to hide and false to show.
@@ -204,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
@@ -245,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() {
@@ -260,13 +341,11 @@
                     }
                     loadWorkspace(new ArrayList<>(), query, null, null);
 
-                    final SparseArray<Size> spanInfo =
-                            getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
-
+                    final SparseArray<Size> spanInfo = getLoadedLauncherWidgetInfo();
                     MAIN_EXECUTOR.execute(() -> {
                         renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo,
                                 idp);
-                        mOnDestroyCallbacks.add(previewContext::onDestroy);
+                        mLifeCycleTracker.add(previewContext::onDestroy);
                     });
                 }
             }.run();
@@ -289,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
@@ -302,11 +386,61 @@
         view.setPivotY(0);
         view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
         view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
-        view.setAlpha(0);
-        view.animate().alpha(1)
-                .setInterpolator(new AccelerateDecelerateInterpolator())
-                .setDuration(FADE_IN_ANIMATION_DURATION)
-                .start();
-        mSurfaceControlViewHost.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());
+        if (!Flags.newCustomizationPickerUi()) {
+            view.setAlpha(0);
+            view.animate().alpha(1)
+                    .setInterpolator(new AccelerateDecelerateInterpolator())
+                    .setDuration(FADE_IN_ANIMATION_DURATION)
+                    .start();
+            mSurfaceControlViewHost.setView(
+                    view,
+                    view.getMeasuredWidth(),
+                    view.getMeasuredHeight()
+            );
+            return;
+        }
+
+        if (mViewRoot == null) {
+            mViewRoot = new FrameLayout(inflationContext);
+            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
+                    FrameLayout.LayoutParams.WRAP_CONTENT, // Width
+                    FrameLayout.LayoutParams.WRAP_CONTENT  // Height
+            );
+            mViewRoot.setLayoutParams(layoutParams);
+            mViewRoot.addView(view);
+            mViewRoot.setAlpha(0);
+            mViewRoot.animate().alpha(1)
+                    .setInterpolator(new AccelerateDecelerateInterpolator())
+                    .setDuration(FADE_IN_ANIMATION_DURATION)
+                    .start();
+            mSurfaceControlViewHost.setView(
+                    mViewRoot,
+                    view.getMeasuredWidth(),
+                    view.getMeasuredHeight()
+            );
+        } else  {
+            mViewRoot.removeAllViews();
+            mViewRoot.addView(view);
+        }
     }
+
+    private static class MySurfaceControlViewHost extends SurfaceControlViewHost {
+
+        private final RunnableList mLifecycleTracker;
+
+        MySurfaceControlViewHost(Context context, Display display, IBinder hostToken,
+                RunnableList lifeCycleTracker) {
+            super(context, display, hostToken);
+            mLifecycleTracker = lifeCycleTracker;
+            mLifecycleTracker.add(this::release);
+        }
+
+        @Override
+        public void release() {
+            super.release();
+            // RunnableList ensures that the callback is only called once
+            MAIN_EXECUTOR.execute(mLifecycleTracker::executeAllAndDestroy);
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
index 260d490..d59fc19 100644
--- a/src/com/android/launcher3/graphics/SysUiScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -18,8 +18,6 @@
 import static android.graphics.Paint.DITHER_FLAG;
 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
 
-import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
-
 import android.animation.ObjectAnimator;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -34,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;
@@ -86,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;
 
@@ -96,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);
@@ -111,7 +109,7 @@
                 new int[]{0x00FFFFFF, 0x2FFFFFFF},
                 new float[]{0f, 1f});
 
-        if (!KEYGUARD_ANIMATION.get() && !mHideSysUiScrim) {
+        if (!mHideSysUiScrim) {
             view.addOnAttachStateChangeListener(this);
         }
     }
@@ -132,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;
             }
@@ -168,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);
     }
 
     /**
@@ -215,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/ComponentWithLabel.java b/src/com/android/launcher3/icons/ComponentWithLabel.java
deleted file mode 100644
index 30575fc..0000000
--- a/src/com/android/launcher3/icons/ComponentWithLabel.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.icons.cache.CachingLogic;
-
-public interface ComponentWithLabel {
-
-    ComponentName getComponent();
-
-    UserHandle getUser();
-
-    CharSequence getLabel(PackageManager pm);
-
-
-    class ComponentCachingLogic<T extends ComponentWithLabel> implements CachingLogic<T> {
-
-        private final PackageManager mPackageManager;
-        private final boolean mAddToMemCache;
-
-        public ComponentCachingLogic(Context context, boolean addToMemCache) {
-            mPackageManager = context.getPackageManager();
-            mAddToMemCache = addToMemCache;
-        }
-
-        @Override
-        @NonNull
-        public ComponentName getComponent(@NonNull T object) {
-            return object.getComponent();
-        }
-
-        @NonNull
-        @Override
-        public UserHandle getUser(@NonNull T object) {
-            return object.getUser();
-        }
-
-        @NonNull
-        @Override
-        public CharSequence getLabel(@NonNull T object) {
-            return object.getLabel(mPackageManager);
-        }
-
-        @NonNull
-        @Override
-        public BitmapInfo loadIcon(@NonNull Context context, @NonNull T object) {
-            return BitmapInfo.LOW_RES_INFO;
-        }
-
-        @Override
-        public boolean addToMemCache() {
-            return mAddToMemCache;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java b/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java
deleted file mode 100644
index 0a52dd7..0000000
--- a/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java
+++ /dev/null
@@ -1,56 +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.icons;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.icons.BaseIconFactory.IconOptions;
-
-/**
- * Extension of ComponentWithLabel to also support loading icons
- */
-public interface ComponentWithLabelAndIcon extends ComponentWithLabel {
-
-    /**
-     * Provide an icon for this object
-     */
-    Drawable getFullResIcon(IconCache cache);
-
-    class ComponentWithIconCachingLogic extends ComponentCachingLogic<ComponentWithLabelAndIcon> {
-
-        public ComponentWithIconCachingLogic(Context context, boolean addToMemCache) {
-            super(context, addToMemCache);
-        }
-
-        @NonNull
-        @Override
-        public BitmapInfo loadIcon(@NonNull Context context,
-                @NonNull ComponentWithLabelAndIcon object) {
-            Drawable d = object.getFullResIcon(LauncherAppState.getInstance(context)
-                    .getIconCache());
-            if (d == null) {
-                return super.loadIcon(context, object);
-            }
-            try (LauncherIcons li = LauncherIcons.obtain(context)) {
-                return li.createBadgedIconBitmap(d, new IconOptions().setUser(object.getUser()));
-            }
-        }
-    }
-}
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 44e448e..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;
@@ -54,9 +50,10 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
 import com.android.launcher3.icons.cache.BaseIconCache;
-import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.icons.cache.CachedObject;
+import com.android.launcher3.icons.cache.CachedObjectCachingLogic;
+import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
@@ -65,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;
@@ -95,14 +92,9 @@
     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;
-    private final IconProvider mIconProvider;
     private final CancellableTask mCancelledTask;
 
     private final SparseArray<BitmapInfo> mWidgetCategoryBitmapInfos;
@@ -112,14 +104,10 @@
     public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName,
             IconProvider iconProvider) {
         super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
-                idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */);
-        mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false);
-        mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context);
-        mShortcutCachingLogic = new ShortcutCachingLogic();
+                idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */, iconProvider);
         mLauncherApps = mContext.getSystemService(LauncherApps.class);
         mUserManager = UserCache.INSTANCE.get(mContext);
         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
-        mIconProvider = iconProvider;
         mWidgetCategoryBitmapInfos = new SparseArray<>();
 
         mCancelledTask = new CancellableTask(() -> null, MAIN_EXECUTOR, c -> { });
@@ -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,14 +314,17 @@
         } else {
             Intent intent = info.getIntent();
             getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user),
-                    true, useLowResIcon, info.isArchived());
+                    true, useLowResIcon);
         }
     }
 
-    public synchronized String getTitleNoCache(ComponentWithLabel info) {
+    /**
+     * Loads and returns the icon for the provided object without adding it to memCache
+     */
+    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);
     }
 
@@ -351,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.
@@ -481,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);
@@ -531,7 +489,7 @@
                     loadFallbackIcon(
                             lai,
                             entry,
-                            mLauncherActivityInfoCachingLogic,
+                            LauncherActivityCachingLogic.INSTANCE,
                             /* usePackageIcon= */ false,
                             /* usePackageTitle= */ loadFallbackTitle,
                             cn,
@@ -541,7 +499,7 @@
                     loadFallbackTitle(
                             lai,
                             entry,
-                            mLauncherActivityInfoCachingLogic,
+                            LauncherActivityCachingLogic.INSTANCE,
                             sectionKey.first);
                 }
 
@@ -600,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) {
@@ -629,10 +589,9 @@
                 info.getAppLabel());
     }
 
-    @Override
-    @NonNull
-    protected String getIconSystemState(String packageName) {
-        return mIconProvider.getSystemStateForPackage(mSystemState, packageName);
+    @VisibleForTesting
+    synchronized boolean isItemInDb(ComponentKey cacheKey) {
+        return getEntryFromDBLocked(cacheKey, new CacheEntry(), false);
     }
 
     /**
diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
deleted file mode 100644
index 406f697..0000000
--- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.LauncherActivityInfo;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BaseIconFactory.IconOptions;
-import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Caching logic for LauncherActivityInfo.
- */
-public class LauncherActivityCachingLogic
-        implements CachingLogic<LauncherActivityInfo>, ResourceBasedOverride {
-
-    /**
-     * Creates and returns a new instance
-     */
-    public static LauncherActivityCachingLogic newInstance(Context context) {
-        return Overrides.getObject(LauncherActivityCachingLogic.class, context,
-                R.string.launcher_activity_logic_class);
-    }
-
-    @NonNull
-    @Override
-    public ComponentName getComponent(@NonNull LauncherActivityInfo object) {
-        return object.getComponentName();
-    }
-
-    @NonNull
-    @Override
-    public UserHandle getUser(@NonNull LauncherActivityInfo object) {
-        return object.getUser();
-    }
-
-    @NonNull
-    @Override
-    public CharSequence getLabel(@NonNull LauncherActivityInfo object) {
-        return object.getLabel();
-    }
-
-    @NonNull
-    @Override
-    public BitmapInfo loadIcon(@NonNull Context context, @NonNull LauncherActivityInfo object) {
-        try (LauncherIcons li = LauncherIcons.obtain(context)) {
-            return li.createBadgedIconBitmap(LauncherAppState.getInstance(context)
-                            .getIconProvider().getIcon(object, li.mFillResIconDpi),
-                    new IconOptions().setUser(object.getUser()));
-        }
-    }
-}
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/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 f40eda6..0000000
--- a/src/com/android/launcher3/icons/ShortcutCachingLogic.java
+++ /dev/null
@@ -1,115 +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.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 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/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
index 480e8f3..ec0efe0 100644
--- a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
@@ -199,6 +199,10 @@
     }
 
     protected void changeFocus(T item, boolean hasFocus) {
+        if (mLastFocusedItem != item && !hasFocus) {
+            return;
+        }
+
         if (hasFocus) {
             endCurrentAnimation();
 
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 64ebbf3..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);
@@ -223,7 +224,8 @@
                     if (DEBUG) {
                         Log.w(TAG, "updatePromiseInstallInfo: removing app due to install"
                                 + " failure and appInfo not startable."
-                                + " package=" + appInfo.getTargetPackage());
+                                + " package=" + appInfo.getTargetPackage()
+                                + ", user=" + user);
                     }
                     removeApp(i);
                 }
@@ -319,7 +321,8 @@
                     if (!findActivity(matches, applicationInfo.componentName)) {
                         if (DEBUG) {
                             Log.w(TAG, "Changing shortcut target due to app component name change."
-                                    + " package=" + packageName);
+                                    + " component=" + applicationInfo.componentName
+                                    + ", user=" + user);
                         }
                         removeApp(i);
                     }
@@ -346,8 +349,9 @@
         } else {
             // Remove all data for this package.
             if (DEBUG) {
-                Log.w(TAG, "updatePromiseInstallInfo: no Activities matched updated package,"
-                        + " removing all apps from package=" + packageName);
+                Log.w(TAG, "updatePackage: no Activities matched updated package,"
+                        + " removing any AppInfo with package=" + packageName
+                        + ", user=" + user);
             }
             for (int i = data.size() - 1; i >= 0; i--) {
                 final AppInfo applicationInfo = data.get(i);
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index e6ade61..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;
@@ -52,18 +54,18 @@
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
 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;
 
 /**
@@ -99,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();
@@ -113,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() {
@@ -195,9 +166,17 @@
         if (!WIDGETS_ENABLED) {
             return;
         }
-        final List<WidgetsListBaseEntry> widgets =
-                mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
-        executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
+        Map<PackageItemInfo, List<WidgetItem>>
+                widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItem();
+        List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
+                .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);
     }
 
     /**
@@ -346,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;
@@ -439,126 +416,4 @@
             });
         }
     }
-
-    private class DisjointWorkspaceBinder {
-        private final IntArray mOrderedScreenIds;
-        private final IntSet mCurrentScreenIds = new IntSet();
-        private final Set<Integer> mBoundItemIds = new HashSet<>();
-
-        protected DisjointWorkspaceBinder(IntArray orderedScreenIds) {
-            mOrderedScreenIds = orderedScreenIds;
-
-            for (Callbacks cb : mCallbacksList) {
-                mCurrentScreenIds.addAll(cb.getPagesToBindSynchronously(orderedScreenIds));
-            }
-            if (mCurrentScreenIds.size() == 0) {
-                mCurrentScreenIds.add(Workspace.FIRST_SCREEN_ID);
-            }
-        }
-
-        /**
-         * Binds the currently loaded items in the Data Model. Also signals to the Callbacks[]
-         * that these items have been bound and their respective screens are ready to be shown.
-         *
-         * If this method is called after all the items on the workspace screen have already been
-         * loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will
-         * not bind any items.
-         */
-        protected void bindCurrentWorkspacePages(boolean isBindSync) {
-            // Save a copy of all the bg-thread collections
-            ArrayList<ItemInfo> workspaceItems;
-            ArrayList<LauncherAppWidgetInfo> appWidgets;
-            ArrayList<FixedContainerItems> fciList = new ArrayList<>();
-            final int workspaceItemCount;
-            synchronized (mBgDataModel) {
-                workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
-                appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
-                if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                    mBgDataModel.extraItems.forEach(fciList::add);
-                }
-                workspaceItemCount = mBgDataModel.itemsIdMap.size();
-            }
-
-            workspaceItems.forEach(it -> mBoundItemIds.add(it.id));
-            appWidgets.forEach(it -> mBoundItemIds.add(it.id));
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                fciList.forEach(item ->
-                        executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
-            }
-
-            sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
-            // Tell the workspace that we're about to start binding items
-            executeCallbacksTask(c -> {
-                c.clearPendingBinds();
-                c.startBinding();
-            }, mUiExecutor);
-
-            // Bind workspace screens
-            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
-
-            bindWorkspaceItems(workspaceItems);
-            bindAppWidgets(appWidgets);
-            executeCallbacksTask(c -> {
-                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
-                RunnableList onCompleteSignal = new RunnableList();
-                onCompleteSignal.executeAllAndDestroy();
-                c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal,
-                        workspaceItemCount, isBindSync);
-            }, mUiExecutor);
-        }
-
-        protected void bindOtherWorkspacePages() {
-            // Save a copy of all the bg-thread collections
-            ArrayList<ItemInfo> workspaceItems;
-            ArrayList<LauncherAppWidgetInfo> appWidgets;
-
-            synchronized (mBgDataModel) {
-                workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
-                appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
-            }
-
-            workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id));
-            appWidgets.removeIf(it -> mBoundItemIds.contains(it.id));
-
-            sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
-            bindWorkspaceItems(workspaceItems);
-            bindAppWidgets(appWidgets);
-
-            executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor);
-            mUiExecutor.execute(() -> {
-                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
-                ItemInstallQueue.INSTANCE.get(mApp.getContext())
-                        .resumeModelPush(FLAG_LOADER_RUNNING);
-            });
-
-            StringCache cacheClone = mBgDataModel.stringCache.clone();
-            executeCallbacksTask(c -> c.bindStringCache(cacheClone), mUiExecutor);
-        }
-
-        private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems) {
-            // Bind the workspace items
-            int count = workspaceItems.size();
-            for (int i = 0; i < count; i += ITEMS_CHUNK) {
-                final int start = i;
-                final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
-                executeCallbacksTask(
-                        c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
-                        mUiExecutor);
-            }
-        }
-
-        private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets) {
-            // Bind the widgets, one at a time
-            int count = appWidgets.size();
-            for (int i = 0; i < count; i++) {
-                final ItemInfo widget = appWidgets.get(i);
-                executeCallbacksTask(
-                        c -> c.bindItems(Collections.singletonList(widget), false),
-                        mUiExecutor);
-            }
-        }
-    }
 }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 9a9fa5b..b9b1e98 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -537,7 +537,13 @@
         default void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
         default void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
         default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { }
-        default void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
+
+        /**
+         * Binds the app widgets to the providers that share widgets with the UI.
+         */
+        default void bindAllWidgets(@NonNull List<WidgetsListBaseEntry> widgets,
+                @NonNull List<WidgetsListBaseEntry> defaultWidgets) {
+        }
         default void bindSmartspaceWidget() { }
 
         /** Called when workspace has been bound. */
diff --git a/src/com/android/launcher3/model/DbEntry.kt b/src/com/android/launcher3/model/DbEntry.kt
new file mode 100644
index 0000000..b79d312
--- /dev/null
+++ b/src/com/android/launcher3/model/DbEntry.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model
+
+import android.content.ContentValues
+import android.content.Intent
+import android.util.Log
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.LauncherSettings.Favorites.CELLX
+import com.android.launcher3.LauncherSettings.Favorites.CELLY
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Favorites.SCREEN
+import com.android.launcher3.LauncherSettings.Favorites.SPANX
+import com.android.launcher3.LauncherSettings.Favorites.SPANY
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ContentWriter
+import java.net.URISyntaxException
+import java.util.Objects
+
+class DbEntry : ItemInfo(), Comparable<DbEntry> {
+    @JvmField var mIntent: String? = null
+    @JvmField var mProvider: String? = null
+    @JvmField var mFolderItems: MutableMap<String, Set<Int>> = HashMap()
+
+    /** Id of the specific widget. */
+    @JvmField var appWidgetId: Int = NO_ID
+
+    /** Comparator according to the reading order */
+    override fun compareTo(other: DbEntry): Int {
+        if (screenId != other.screenId) {
+            return screenId.compareTo(other.screenId)
+        }
+        if (cellY != other.cellY) {
+            return cellY.compareTo(other.cellY)
+        }
+        return cellX.compareTo(other.cellX)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DbEntry) return false
+        return getEntryMigrationId() == other.getEntryMigrationId()
+    }
+
+    override fun hashCode(): Int = Objects.hash(getEntryMigrationId())
+
+    /**
+     * Puts the updated DbEntry values into ContentValues which we then use to insert the entry to
+     * the DB.
+     */
+    fun updateContentValues(values: ContentValues) =
+        values.apply {
+            put(SCREEN, screenId)
+            put(CELLX, cellX)
+            put(CELLY, cellY)
+            put(SPANX, spanX)
+            put(SPANY, spanY)
+        }
+
+    override fun writeToValues(writer: ContentWriter) {
+        super.writeToValues(writer)
+        writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId)
+    }
+
+    override fun readFromValues(values: ContentValues) {
+        super.readFromValues(values)
+        appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID)
+    }
+
+    /**
+     * This id is not used in the DB is only used while doing the migration and it identifies an
+     * entry on each workspace. For example two calculator icons would have the same migration id
+     * even thought they have different database ids.
+     */
+    private fun getEntryMigrationId(): String? {
+        when (itemType) {
+            ITEM_TYPE_FOLDER,
+            ITEM_TYPE_APP_PAIR -> return getFolderMigrationId()
+            ITEM_TYPE_APPWIDGET ->
+                // mProvider is the app the widget belongs to and appWidgetId it's the unique
+                // is of the widget, we need both because if you remove a widget and then add it
+                // again, then it can change and the WidgetProvider would not know the widget.
+                return mProvider + appWidgetId
+            ITEM_TYPE_APPLICATION -> {
+                val intentStr = mIntent?.let { cleanIntentString(it) }
+                try {
+                    val i = Intent.parseUri(intentStr, 0)
+                    return Objects.requireNonNull(i.component).toString()
+                } catch (e: Exception) {
+                    return intentStr
+                }
+            }
+
+            else -> return mIntent?.let { cleanIntentString(it) }
+        }
+    }
+
+    /**
+     * This method should return an id that should be the same for two folders containing the same
+     * elements.
+     */
+    private fun getFolderMigrationId(): String =
+        mFolderItems.keys
+            .map { intentString: String ->
+                mFolderItems[intentString]?.size.toString() + cleanIntentString(intentString)
+            }
+            .sorted()
+            .joinToString(",")
+
+    /**
+     * This is needed because sourceBounds can change and make the id of two equal items different.
+     */
+    private fun cleanIntentString(intentStr: String): String {
+        try {
+            return Intent.parseUri(intentStr, 0).apply { sourceBounds = null }.toURI()
+        } catch (e: URISyntaxException) {
+            Log.e(TAG, "Unable to parse Intent string", e)
+            return intentStr
+        }
+    }
+
+    companion object {
+        private const val TAG = "DbEntry"
+    }
+}
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 729b381..90af215 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -156,10 +156,16 @@
     }
 
     public Integer getColumns() {
+        if (TextUtils.isEmpty(mGridSizeString)) {
+            return -1;
+        }
         return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[0]));
     }
 
     public Integer getRows() {
+        if (TextUtils.isEmpty(mGridSizeString)) {
+            return -1;
+        }
         return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[1]));
     }
 
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
similarity index 73%
rename from src/com/android/launcher3/model/GridSizeMigrationUtil.java
rename to src/com/android/launcher3/model/GridSizeMigrationDBController.java
index ad32fc2..617cac7 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -27,9 +27,6 @@
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
@@ -40,22 +37,17 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.pm.InstallSessionHelper;
 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;
@@ -63,7 +55,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -71,12 +62,12 @@
  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
  * result of restoring from a larger device or device density change.
  */
-public class GridSizeMigrationUtil {
+public class GridSizeMigrationDBController {
 
-    private static final String TAG = "GridSizeMigrationUtil";
+    private static final String TAG = "GridSizeMigrationDBController";
     private static final boolean DEBUG = true;
 
-    private GridSizeMigrationUtil() {
+    private GridSizeMigrationDBController() {
         // Util class should not be instantiated
     }
 
@@ -87,20 +78,26 @@
         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) {
-        DbReader dbReader = new DbReader(db, tableName, context, getValidPackages(context));
+        DbReader dbReader = new DbReader(db, tableName, context);
         List<DbEntry> result = dbReader.loadAllWorkspaceEntries();
         result.addAll(dbReader.loadHotseatEntries());
         return result;
@@ -120,12 +117,14 @@
             @NonNull DeviceGridState srcDeviceState,
             @NonNull DeviceGridState destDeviceState,
             @NonNull DatabaseHelper target,
-            @NonNull SQLiteDatabase source) {
+            @NonNull SQLiteDatabase source,
+            boolean isDestNewDb) {
+
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
             return true;
         }
 
-        if (Flags.enableGridMigrationFix()
+        if (isDestNewDb
                 && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
                 && srcDeviceState.getRows() < destDeviceState.getRows()) {
             // Only use this strategy when comparing the previous grid to the new grid and the
@@ -136,11 +135,10 @@
         }
         copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
 
-        HashSet<String> validPackages = getValidPackages(context);
         long migrationStartTime = System.currentTimeMillis();
         try (SQLiteTransaction t = new SQLiteTransaction(target.getWritableDatabase())) {
-            DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context, validPackages);
-            DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context, validPackages);
+            DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context);
+            DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context);
 
             Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows());
             migrate(target, srcReader, destReader, destDeviceState.getNumHotseat(),
@@ -192,7 +190,7 @@
                     Collectors.joining(",\n", "[", "]"))
                     + "\n Removing Items:"
                     + dstWorkspaceItems.stream().filter(entry ->
-                            toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
+                    toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
                     Collectors.joining(",\n", "[", "]"))
                     + "\n Adding Workspace Items:"
                     + workspaceToBeAdded.stream().map(DbEntry::toString).collect(
@@ -213,9 +211,13 @@
         Collections.sort(hotseatToBeAdded);
         Collections.sort(workspaceToBeAdded);
 
+        List<Integer> idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect(
+                Collectors.toList());
+        idsInUse.addAll(dstHotseatItems.stream().map(entry -> entry.id).toList());
+
         // Migrate hotseat
         solveHotseatPlacement(helper, destHotseatSize,
-                srcReader, destReader, dstHotseatItems, hotseatToBeAdded);
+                srcReader, destReader, dstHotseatItems, hotseatToBeAdded, idsInUse);
 
         // Migrate workspace.
         // First we create a collection of the screens
@@ -230,7 +232,7 @@
                 Log.d(TAG, "Migrating " + screenId);
             }
             solveGridPlacement(helper, srcReader,
-                    destReader, screenId, trgX, trgY, workspaceToBeAdded);
+                    destReader, screenId, trgX, trgY, workspaceToBeAdded, idsInUse);
             if (workspaceToBeAdded.isEmpty()) {
                 break;
             }
@@ -241,7 +243,7 @@
         int screenId = destReader.mLastScreenId + 1;
         while (!workspaceToBeAdded.isEmpty()) {
             solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY,
-                    workspaceToBeAdded);
+                    workspaceToBeAdded, srcWorkspaceItems.stream().map(entry -> entry.id).toList());
             screenId++;
         }
 
@@ -257,47 +259,57 @@
     private static void calcDiff(@NonNull final List<DbEntry> src,
             @NonNull final List<DbEntry> dest, @NonNull final List<DbEntry> toBeAdded,
             @NonNull final IntArray toBeRemoved) {
+        HashMap<DbEntry, Integer> entryCountDiff = new HashMap<>();
+        src.forEach(entry ->
+                entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) + 1));
+        dest.forEach(entry ->
+                entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) - 1));
+
         src.forEach(entry -> {
-            if (!dest.contains(entry)) {
+            if (entryCountDiff.get(entry) > 0) {
                 toBeAdded.add(entry);
+                entryCountDiff.put(entry, entryCountDiff.get(entry) - 1);
             }
         });
+
         dest.forEach(entry -> {
-            if (!src.contains(entry)) {
+            if (entryCountDiff.get(entry) < 0) {
                 toBeRemoved.add(entry.id);
                 if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
                     entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add));
                 }
+                entryCountDiff.put(entry, entryCountDiff.get(entry) + 1);
             }
         });
     }
 
-    private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
-            String srcTableName, String destTableName) {
-        int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName);
-
+    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
                 || entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) {
             for (Set<Integer> itemIds : entry.mFolderItems.values()) {
                 for (int itemId : itemIds) {
-                    copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName);
+                    copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName, idsInUse);
                 }
             }
         }
     }
 
     private static int copyEntryAndUpdate(DatabaseHelper helper,
-            DbEntry entry, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(helper, entry, -1, -1, srcTableName, destTableName);
+            DbEntry entry, String srcTableName, String destTableName, List<Integer> idsInUse) {
+        return copyEntryAndUpdate(
+                helper, entry, -1, -1, srcTableName, destTableName, idsInUse);
     }
 
-    private static int copyEntryAndUpdate(DatabaseHelper helper,
-            int id, int folderId, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(helper, null, id, folderId, srcTableName, destTableName);
+    private static int copyEntryAndUpdate(DatabaseHelper helper, int id,
+            int folderId, String srcTableName, String destTableName, List<Integer> idsInUse) {
+        return copyEntryAndUpdate(
+                helper, null, id, folderId, srcTableName, destTableName, idsInUse);
     }
 
-    private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry,
-            int id, int folderId, String srcTableName, String destTableName) {
+    private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry, int id,
+            int folderId, String srcTableName, String destTableName, List<Integer> idsInUse) {
         int newId = -1;
         Cursor c = helper.getWritableDatabase().query(srcTableName, null,
                 LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'",
@@ -310,7 +322,9 @@
             } else {
                 values.put(LauncherSettings.Favorites.CONTAINER, folderId);
             }
-            newId = helper.generateNewItemId();
+            do {
+                newId = helper.generateNewItemId();
+            } while (idsInUse.contains(newId));
             values.put(LauncherSettings.Favorites._ID, newId);
             helper.getWritableDatabase().insert(destTableName, null, values);
         }
@@ -318,32 +332,15 @@
         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);
     }
 
-    private static HashSet<String> getValidPackages(Context context) {
-        // Initialize list of valid packages. This contain all the packages which are already on
-        // the device and packages which are being installed. Any item which doesn't belong to
-        // this set is removed.
-        // Since the loader removes such items anyway, removing these items here doesn't cause
-        // any extra data loss and gives us more free space on the grid for better migration.
-        HashSet<String> validPackages = new HashSet<>();
-        for (PackageInfo info : context.getPackageManager()
-                .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
-            validPackages.add(info.packageName);
-        }
-        InstallSessionHelper.INSTANCE.get(context)
-                .getActiveSessions().keySet()
-                .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
-        return validPackages;
-    }
-
     private static void solveGridPlacement(@NonNull final DatabaseHelper helper,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
             final int screenId, final int trgX, final int trgY,
-            @NonNull final List<DbEntry> sortedItemsToPlace) {
+            @NonNull final List<DbEntry> sortedItemsToPlace, List<Integer> idsInUse) {
         final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
         final Point trg = new Point(trgX, trgY);
         final Point next = new Point(0, screenId == 0
@@ -366,7 +363,8 @@
                 continue;
             }
             if (findPlacementForEntry(entry, next, trg, occupied, screenId)) {
-                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(
+                        helper, entry, srcReader.mTableName, destReader.mTableName, idsInUse);
                 iterator.remove();
             }
         }
@@ -380,7 +378,7 @@
     private static boolean findPlacementForEntry(@NonNull final DbEntry entry,
             @NonNull final Point next, @NonNull final Point trg,
             @NonNull final GridOccupancy occupied, final int screenId) {
-        for (int y = next.y; y <  trg.y; y++) {
+        for (int y = next.y; y < trg.y; y++) {
             for (int x = next.x; x < trg.x; x++) {
                 boolean fits = occupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
                 boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX,
@@ -406,8 +404,8 @@
     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> itemsToPlace) {
+            @NonNull final List<DbEntry> placedHotseatItems,
+            @NonNull final List<DbEntry> itemsToPlace, List<Integer> idsInUse) {
 
         final boolean[] occupied = new boolean[hotseatSize];
         for (DbEntry entry : placedHotseatItems) {
@@ -422,30 +420,39 @@
                 // to something other than -1.
                 entry.cellX = i;
                 entry.cellY = 0;
-                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(
+                        helper, entry, srcReader.mTableName, destReader.mTableName, idsInUse);
                 occupied[entry.screenId] = true;
             }
         }
     }
 
-    @VisibleForTesting
+    static void copyCurrentGridToNewGrid(
+            @NonNull Context context,
+            @NonNull DeviceGridState destDeviceState,
+            @NonNull DatabaseHelper target,
+            @NonNull SQLiteDatabase source) {
+        // Only use this strategy when comparing the previous grid to the new grid and the
+        // columns are the same and the destination has more rows
+        copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
+        destDeviceState.writeToPrefs(context);
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
     public static class DbReader {
 
-        private final SQLiteDatabase mDb;
-        private final String mTableName;
-        private final Context mContext;
-        private final Set<String> mValidPackages;
-        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,
-                Set<String> validPackages) {
+        public DbReader(SQLiteDatabase db, String tableName, Context context) {
             mDb = db;
             mTableName = tableName;
             mContext = context;
-            mValidPackages = validPackages;
         }
 
         protected List<DbEntry> loadHotseatEntries() {
@@ -477,7 +484,6 @@
                         case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                         case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
                             entry.mIntent = c.getString(indexIntent);
-                            verifyIntent(c.getString(indexIntent));
                             break;
                         }
                         case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
@@ -525,7 +531,7 @@
                             LauncherSettings.Favorites.INTENT,               // 7
                             LauncherSettings.Favorites.APPWIDGET_PROVIDER,   // 8
                             LauncherSettings.Favorites.APPWIDGET_ID},        // 9
-                        LauncherSettings.Favorites.CONTAINER + " = "
+                    LauncherSettings.Favorites.CONTAINER + " = "
                             + LauncherSettings.Favorites.CONTAINER_DESKTOP);
             final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
             final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
@@ -559,17 +565,15 @@
                         case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                         case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
                             entry.mIntent = c.getString(indexIntent);
-                            verifyIntent(entry.mIntent);
                             break;
                         }
                         case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: {
                             entry.mProvider = c.getString(indexAppWidgetProvider);
+                            entry.appWidgetId = c.getInt(indexAppWidgetId);
                             ComponentName cn = ComponentName.unflattenFromString(entry.mProvider);
-                            verifyPackage(cn.getPackageName());
 
-                            int widgetId = c.getInt(indexAppWidgetId);
                             LauncherAppWidgetProviderInfo pInfo = widgetManagerHelper
-                                    .getLauncherAppWidgetInfo(widgetId, cn);
+                                    .getLauncherAppWidgetInfo(entry.appWidgetId, cn);
                             Point spans = null;
                             if (pInfo != null) {
                                 spans = pInfo.getMinSpans();
@@ -629,7 +633,6 @@
                 try {
                     int id = c.getInt(0);
                     String intent = c.getString(1);
-                    verifyIntent(intent);
                     total++;
                     if (!entry.mFolderItems.containsKey(intent)) {
                         entry.mFolderItems.put(intent, new HashSet<>());
@@ -646,140 +649,5 @@
         private Cursor queryWorkspace(String[] columns, String where) {
             return mDb.query(mTableName, columns, where, null, null, null, null);
         }
-
-        /** Verifies if the mIntent should be restored. */
-        private void verifyIntent(String intentStr)
-                throws Exception {
-            Intent intent = Intent.parseUri(intentStr, 0);
-            if (intent.getComponent() != null) {
-                verifyPackage(intent.getComponent().getPackageName());
-            } else if (intent.getPackage() != null) {
-                // Only verify package if the component was null.
-                verifyPackage(intent.getPackage());
-            }
-        }
-
-        /** Verifies if the package should be restored */
-        private void verifyPackage(String packageName)
-                throws Exception {
-            if (!mValidPackages.contains(packageName)) {
-                // TODO(b/151468819): Handle promise app icon restoration during grid migration.
-                throw new Exception("Package not available");
-            }
-        }
-    }
-
-    public static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
-
-        private String mIntent;
-        private String mProvider;
-        private Map<String, Set<Integer>> mFolderItems = new HashMap<>();
-
-        /**
-         * Id of the specific widget.
-         */
-        public int appWidgetId = NO_ID;
-
-        /** Comparator according to the reading order */
-        @Override
-        public int compareTo(DbEntry another) {
-            if (screenId != another.screenId) {
-                return Integer.compare(screenId, another.screenId);
-            }
-            if (cellY != another.cellY) {
-                return Integer.compare(cellY, another.cellY);
-            }
-            return Integer.compare(cellX, another.cellX);
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            DbEntry entry = (DbEntry) o;
-            return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(getEntryMigrationId());
-        }
-
-        public void updateContentValues(ContentValues values) {
-            values.put(LauncherSettings.Favorites.SCREEN, screenId);
-            values.put(LauncherSettings.Favorites.CELLX, cellX);
-            values.put(LauncherSettings.Favorites.CELLY, cellY);
-            values.put(LauncherSettings.Favorites.SPANX, spanX);
-            values.put(LauncherSettings.Favorites.SPANY, spanY);
-        }
-
-        @Override
-        public void writeToValues(@NonNull ContentWriter writer) {
-            super.writeToValues(writer);
-            writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
-        }
-
-        @Override
-        public void readFromValues(@NonNull ContentValues values) {
-            super.readFromValues(values);
-            appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID);
-        }
-
-        /** This id is not used in the DB is only used while doing the migration and it identifies
-         * an entry on each workspace. For example two calculator icons would have the same
-         * migration id even thought they have different database ids.
-         */
-        public String getEntryMigrationId() {
-            switch (itemType) {
-                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
-                    return getFolderMigrationId();
-                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                    // mProvider is the app the widget belongs to and appWidgetId it's the unique
-                    // is of the widget, we need both because if you remove a widget and then add it
-                    // again, then it can change and the WidgetProvider would not know the widget.
-                    return mProvider + appWidgetId;
-                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                    final String intentStr = cleanIntentString(mIntent);
-                    try {
-                        Intent i = Intent.parseUri(intentStr, 0);
-                        return Objects.requireNonNull(i.getComponent()).toString();
-                    } catch (Exception e) {
-                        return intentStr;
-                    }
-                default:
-                    return cleanIntentString(mIntent);
-            }
-        }
-
-        /**
-         * This method should return an id that should be the same for two folders containing the
-         * same elements.
-         */
-        @NonNull
-        private String getFolderMigrationId() {
-            return mFolderItems.keySet().stream()
-                    .map(intentString -> mFolderItems.get(intentString).size()
-                            + cleanIntentString(intentString))
-                    .sorted()
-                    .collect(Collectors.joining(","));
-        }
-
-        /**
-         * This is needed because sourceBounds can change and make the id of two equal items
-         * different.
-         */
-        @NonNull
-        private String cleanIntentString(@NonNull String intentStr) {
-            try {
-                Intent i = Intent.parseUri(intentStr, 0);
-                i.setSourceBounds(null);
-                return i.toURI();
-            } catch (URISyntaxException e) {
-                Log.e(TAG, "Unable to parse Intent string", e);
-                return intentStr;
-            }
-
-        }
     }
 }
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
new file mode 100644
index 0000000..c856d4b
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -0,0 +1,525 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.get
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
+import com.android.launcher3.provider.LauncherDbUtils
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.util.GridOccupancy
+import com.android.launcher3.util.IntArray
+
+class GridSizeMigrationLogic {
+    /**
+     * Migrates the grid size from srcDeviceState to destDeviceState and make those changes in the
+     * target DB, using the source DB to determine what to add/remove/move/resize in the destination
+     * DB.
+     */
+    fun migrateGrid(
+        context: Context,
+        srcDeviceState: DeviceGridState,
+        destDeviceState: DeviceGridState,
+        target: DatabaseHelper,
+        source: SQLiteDatabase,
+        isDestNewDb: Boolean,
+    ) {
+        if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) {
+            return
+        }
+
+        val isFirstLoad = get(context).get(LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE)
+        Log.d(TAG, "Begin grid migration. First load: $isFirstLoad")
+
+        // This is a special case where if the grid is the same amount of columns but a larger
+        // amount of rows we simply copy over the source grid to the destination grid, rather
+        // than undergoing the general grid migration.
+        if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
+            GridSizeMigrationDBController.copyCurrentGridToNewGrid(
+                context,
+                destDeviceState,
+                target,
+                source,
+            )
+            return
+        }
+        LauncherDbUtils.copyTable(
+            source,
+            LauncherSettings.Favorites.TABLE_NAME,
+            target.writableDatabase,
+            LauncherSettings.Favorites.TMP_TABLE,
+            context,
+        )
+
+        val migrationStartTime = System.currentTimeMillis()
+        try {
+            SQLiteTransaction(target.writableDatabase).use { t ->
+                val srcReader = DbReader(t.db, LauncherSettings.Favorites.TMP_TABLE, context)
+                val destReader = DbReader(t.db, LauncherSettings.Favorites.TABLE_NAME, context)
+
+                val targetSize = Point(destDeviceState.columns, destDeviceState.rows)
+
+                // Here we keep all the DB ids we have in the destination DB such that we don't
+                // assign
+                // an item that we want to add to the destination DB the same id as an already
+                // existing
+                // item.
+                val idsInUse = mutableListOf<Int>()
+
+                // Migrate hotseat.
+                migrateHotseat(destDeviceState.numHotseat, srcReader, destReader, target, idsInUse)
+                // Migrate workspace.
+                migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
+
+                LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE)
+                t.commit()
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Error during grid migration", e)
+        } finally {
+            Log.v(
+                TAG,
+                "Workspace migration completed in " +
+                    (System.currentTimeMillis() - migrationStartTime),
+            )
+
+            // Save current configuration, so that the migration does not run again.
+            destDeviceState.writeToPrefs(context)
+        }
+    }
+
+    /** Handles hotseat migration. */
+    @VisibleForTesting
+    fun migrateHotseat(
+        destHotseatSize: Int,
+        srcReader: DbReader,
+        destReader: DbReader,
+        helper: DatabaseHelper,
+        idsInUse: MutableList<Int>,
+    ) {
+        val srcHotseatItems = srcReader.loadHotseatEntries()
+        val dstHotseatItems = destReader.loadHotseatEntries()
+
+        val hotseatToBeAdded = getItemsToBeAdded(srcHotseatItems, dstHotseatItems)
+        val toBeRemoved = IntArray()
+        toBeRemoved.addAll(getItemsToBeRemoved(srcHotseatItems, dstHotseatItems))
+
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                """Start hotseat migration:
+            |Removing Hotseat Items: [${dstHotseatItems.filter { toBeRemoved.contains(it.id) }
+                .joinToString(",\n") { it.toString() }}]
+            |Adding Hotseat Items: [${hotseatToBeAdded
+                .joinToString(",\n") { it.toString() }}]
+            |"""
+                    .trimMargin(),
+            )
+        }
+
+        // Removes the items that we need to remove from the destination DB.
+        if (!toBeRemoved.isEmpty) {
+            GridSizeMigrationDBController.removeEntryFromDb(
+                destReader.mDb,
+                destReader.mTableName,
+                toBeRemoved,
+            )
+        }
+
+        placeHotseatItems(
+            hotseatToBeAdded,
+            dstHotseatItems,
+            destHotseatSize,
+            helper,
+            srcReader,
+            destReader,
+            idsInUse,
+        )
+    }
+
+    private fun placeHotseatItems(
+        hotseatToBeAdded: MutableList<DbEntry>,
+        dstHotseatItems: List<DbEntry>,
+        destHotseatSize: Int,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: MutableList<Int>,
+    ) {
+        if (hotseatToBeAdded.isEmpty()) {
+            return
+        }
+
+        idsInUse.addAll(dstHotseatItems.map { entry: DbEntry -> entry.id })
+
+        hotseatToBeAdded.sort()
+
+        val placementSolutionHotseat =
+            solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded)
+        for (entryToPlace in placementSolutionHotseat) {
+            GridSizeMigrationDBController.insertEntryInDb(
+                helper,
+                entryToPlace,
+                srcReader.mTableName,
+                destReader.mTableName,
+                idsInUse,
+            )
+        }
+    }
+
+    @VisibleForTesting
+    fun migrateWorkspace(
+        srcReader: DbReader,
+        destReader: DbReader,
+        helper: DatabaseHelper,
+        targetSize: Point,
+        idsInUse: MutableList<Int>,
+    ) {
+        val srcWorkspaceItems = srcReader.loadAllWorkspaceEntries()
+
+        val dstWorkspaceItems = destReader.loadAllWorkspaceEntries()
+
+        val toBeRemoved = IntArray()
+
+        val workspaceToBeAdded = getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems)
+        toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems))
+
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                """Start workspace migration:
+            |Source Device: [${srcWorkspaceItems.joinToString(",\n") { it.toString() }}]
+            |Target Device: [${dstWorkspaceItems.joinToString(",\n") { it.toString() }}]
+            |Removing Workspace Items: [${dstWorkspaceItems.filter { toBeRemoved.contains(it.id) }
+                .joinToString(",\n") { it.toString() }}]
+            |Adding Workspace Items: [${workspaceToBeAdded
+                .joinToString(",\n") { it.toString() }}]
+            |"""
+                    .trimMargin(),
+            )
+        }
+
+        // Removes the items that we need to remove from the destination DB.
+        if (!toBeRemoved.isEmpty) {
+            GridSizeMigrationDBController.removeEntryFromDb(
+                destReader.mDb,
+                destReader.mTableName,
+                toBeRemoved,
+            )
+        }
+
+        placeWorkspaceItems(
+            workspaceToBeAdded,
+            dstWorkspaceItems,
+            targetSize.x,
+            targetSize.y,
+            helper,
+            srcReader,
+            destReader,
+            idsInUse,
+        )
+    }
+
+    private fun placeWorkspaceItems(
+        workspaceToBeAdded: MutableList<DbEntry>,
+        dstWorkspaceItems: List<DbEntry>,
+        trgX: Int,
+        trgY: Int,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: MutableList<Int>,
+    ) {
+        if (workspaceToBeAdded.isEmpty()) {
+            return
+        }
+
+        idsInUse.addAll(dstWorkspaceItems.map { entry: DbEntry -> entry.id })
+
+        workspaceToBeAdded.sort()
+
+        // First we create a collection of the screens
+        val screens: MutableList<Int> = ArrayList()
+        for (screenId in 0..destReader.mLastScreenId) {
+            screens.add(screenId)
+        }
+
+        // Then we place the items on the screens
+        var itemsToPlace = WorkspaceItemsToPlace(workspaceToBeAdded, mutableListOf())
+        for (screenId in screens) {
+            if (DEBUG) {
+                Log.d(TAG, "Migrating $screenId")
+            }
+            itemsToPlace =
+                solveGridPlacement(
+                    destReader.mContext,
+                    screenId,
+                    trgX,
+                    trgY,
+                    itemsToPlace.mRemainingItemsToPlace,
+                    destReader.mWorkspaceEntriesByScreenId[screenId],
+                )
+            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+            while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+                GridSizeMigrationDBController.insertEntryInDb(
+                    helper,
+                    itemsToPlace.mPlacementSolution.removeAt(0),
+                    srcReader.mTableName,
+                    destReader.mTableName,
+                    idsInUse,
+                )
+            }
+            if (itemsToPlace.mRemainingItemsToPlace.isEmpty()) {
+                break
+            }
+        }
+
+        // In case the new grid is smaller, there might be some leftover items that don't fit on
+        // any of the screens, in this case we add them to new screens until all of them are placed.
+        var screenId = destReader.mLastScreenId + 1
+        while (itemsToPlace.mRemainingItemsToPlace.isNotEmpty()) {
+            itemsToPlace =
+                solveGridPlacement(
+                    destReader.mContext,
+                    screenId,
+                    trgX,
+                    trgY,
+                    itemsToPlace.mRemainingItemsToPlace,
+                    destReader.mWorkspaceEntriesByScreenId[screenId],
+                )
+            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+            screenId++
+        }
+    }
+
+    private fun placeItems(
+        itemsToPlace: WorkspaceItemsToPlace,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: List<Int>,
+    ) {
+        while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+            GridSizeMigrationDBController.insertEntryInDb(
+                helper,
+                itemsToPlace.mPlacementSolution.removeAt(0),
+                srcReader.mTableName,
+                destReader.mTableName,
+                idsInUse,
+            )
+        }
+    }
+
+    /** Only migrate the grid in this manner if the target grid is taller and not wider. */
+    private fun shouldMigrateToStrictlyTallerGrid(
+        isDestNewDb: Boolean,
+        srcDeviceState: DeviceGridState,
+        destDeviceState: DeviceGridState,
+    ): Boolean {
+        return isDestNewDb &&
+            srcDeviceState.columns == destDeviceState.columns &&
+            srcDeviceState.rows < destDeviceState.rows
+    }
+
+    /**
+     * Finds all the items that are in the old grid which aren't in the new grid, meaning they need
+     * to be added to the new grid.
+     *
+     * @return a list of DbEntry's which we need to add.
+     */
+    private fun getItemsToBeAdded(src: List<DbEntry>, dest: List<DbEntry>): MutableList<DbEntry> {
+        val entryCountDiff = calcDiff(src, dest)
+        val toBeAdded: MutableList<DbEntry> = ArrayList()
+        src.forEach { entry ->
+            entryCountDiff[entry]?.let { entryDiff ->
+                if (entryDiff > 0) {
+                    toBeAdded.add(entry)
+                    entryCountDiff[entry] = entryDiff - 1
+                }
+            }
+        }
+        return toBeAdded
+    }
+
+    /**
+     * Finds all the items that are in the new grid which aren't in the old grid, meaning they need
+     * to be removed from the new grid.
+     *
+     * @return an IntArray of item id's which we need to remove.
+     */
+    private fun getItemsToBeRemoved(src: List<DbEntry>, dest: List<DbEntry>): IntArray {
+        val entryCountDiff = calcDiff(src, dest)
+        val toBeRemoved =
+            IntArray().apply {
+                dest.forEach { entry ->
+                    entryCountDiff[entry]?.let { entryDiff ->
+                        if (entryDiff < 0) {
+                            add(entry.id)
+                            if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                                entry.mFolderItems.values.forEach { ids ->
+                                    ids.forEach { value -> add(value) }
+                                }
+                            }
+                        }
+                        entryCountDiff[entry] = entryDiff.plus(1)
+                    }
+                }
+            }
+        return toBeRemoved
+    }
+
+    /**
+     * Calculates the difference between the old and new grid items in terms of how many of each
+     * item there are. E.g. if the old grid had 2 Calculator icons but the new grid has 0, then the
+     * difference there would be 2. While if the old grid has 0 Calculator icons and the new grid
+     * has 1, then the difference would be -1.
+     *
+     * @return a Map with each DbEntry as a key and the count of said entry as the value.
+     */
+    private fun calcDiff(src: List<DbEntry>, dest: List<DbEntry>): MutableMap<DbEntry, Int> {
+        val entryCountDiff: MutableMap<DbEntry, Int> = HashMap()
+        src.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) + 1 }
+        dest.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) - 1 }
+        return entryCountDiff
+    }
+
+    private fun solveHotseatPlacement(
+        hotseatSize: Int,
+        placedHotseatItems: List<DbEntry>,
+        itemsToPlace: List<DbEntry>,
+    ): List<DbEntry> {
+        val placementSolution: MutableList<DbEntry> = ArrayList()
+        val remainingItemsToPlace: MutableList<DbEntry> = ArrayList(itemsToPlace)
+        val occupied = BooleanArray(hotseatSize)
+        for (entry in placedHotseatItems) {
+            occupied[entry.screenId] = true
+        }
+
+        for (i in occupied.indices) {
+            if (!occupied[i] && remainingItemsToPlace.isNotEmpty()) {
+                val entry: DbEntry =
+                    remainingItemsToPlace.removeAt(0).apply {
+                        screenId = i
+                        // These values does not affect the item position, but we should set them
+                        // to something other than -1.
+                        cellX = i
+                        cellY = 0
+                    }
+                placementSolution.add(entry)
+                occupied[entry.screenId] = true
+            }
+        }
+        return placementSolution
+    }
+
+    private fun solveGridPlacement(
+        context: Context,
+        screenId: Int,
+        trgX: Int,
+        trgY: Int,
+        sortedItemsToPlace: MutableList<DbEntry>,
+        existedEntries: MutableList<DbEntry>?,
+    ): WorkspaceItemsToPlace {
+        val itemsToPlace = WorkspaceItemsToPlace(sortedItemsToPlace, mutableListOf())
+        val occupied = GridOccupancy(trgX, trgY)
+        val trg = Point(trgX, trgY)
+        val next: Point =
+            if (
+                screenId == 0 &&
+                    (FeatureFlags.QSB_ON_FIRST_SCREEN &&
+                        (!Flags.enableSmartspaceRemovalToggle() ||
+                            getPrefs(context)
+                                .getBoolean(LoaderTask.SMARTSPACE_ON_HOME_SCREEN, true)) &&
+                        !Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET)
+            ) {
+                Point(0, 1 /* smartspace */)
+            } else {
+                Point(0, 0)
+            }
+        if (existedEntries != null) {
+            for (entry in existedEntries) {
+                occupied.markCells(entry, true)
+            }
+        }
+        val iterator = itemsToPlace.mRemainingItemsToPlace.iterator()
+        while (iterator.hasNext()) {
+            val entry = iterator.next()
+            if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
+                iterator.remove()
+                continue
+            }
+            findPlacementForEntry(entry, next.x, next.y, trg, occupied)?.let {
+                entry.screenId = screenId
+                entry.cellX = it.cellX
+                entry.cellY = it.cellY
+                entry.spanX = it.spanX
+                entry.spanY = it.spanY
+                occupied.markCells(entry, true)
+                next[entry.cellX + entry.spanX] = entry.cellY
+                itemsToPlace.mPlacementSolution.add(entry)
+                iterator.remove()
+            }
+        }
+        return itemsToPlace
+    }
+
+    /**
+     * Search for the next possible placement of an item. (mNextStartX, mNextStartY) serves as a
+     * memoization of last placement, we can start our search for next placement from there to speed
+     * up the search.
+     *
+     * @return NewEntryPlacement object if we found a valid placement, null if we didn't.
+     */
+    private fun findPlacementForEntry(
+        entry: DbEntry,
+        startPosX: Int,
+        startPosY: Int,
+        trg: Point,
+        occupied: GridOccupancy,
+    ): CellAndSpan? {
+        var newStartPosX = startPosX
+        for (y in startPosY until trg.y) {
+            for (x in newStartPosX until trg.x) {
+                if (occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY)) {
+                    return (CellAndSpan(x, y, entry.minSpanX, entry.minSpanY))
+                }
+            }
+            newStartPosX = 0
+        }
+        return null
+    }
+
+    private data class WorkspaceItemsToPlace(
+        val mRemainingItemsToPlace: MutableList<DbEntry>,
+        val mPlacementSolution: MutableList<DbEntry>,
+    )
+
+    companion object {
+        private const val TAG = "GridSizeMigrationLogic"
+        private const val DEBUG = true
+    }
+}
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index 551c2d8..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;
@@ -192,22 +194,18 @@
     }
 
     private void queuePendingShortcutInfo(PendingInstallShortcutInfo info) {
-        final Exception stackTrace = new Exception();
 
         // Queue the item up for adding if launcher has not loaded properly yet
         MODEL_EXECUTOR.post(() -> {
             Pair<ItemInfo, Object> itemInfo = info.getItemInfo(mContext);
             if (itemInfo == null) {
                 FileLog.d(LOG,
-                        "Adding PendingInstallShortcutInfo with no attached info to queue.",
-                        stackTrace);
+                        "Adding PendingInstallShortcutInfo with no attached info to queue.");
             } else {
                 FileLog.d(LOG,
-                        "Adding PendingInstallShortcutInfo to queue. Attached info: "
-                                + itemInfo.first,
-                        stackTrace);
+                        "Adding PendingInstallShortcutInfo to queue."
+                                + " Attached info: " + itemInfo.first);
             }
-
             addToQueue(info);
         });
         flushInstallQueue();
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 312c6f4..a830c96 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -20,9 +20,11 @@
 import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
 import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.icons.CacheableShortcutInfo.convertShortcutsToCacheableShortcuts;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
@@ -69,12 +71,13 @@
 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.ComponentWithLabelAndIcon.ComponentWithIconCachingLogic;
+import com.android.launcher3.icons.CacheableShortcutCachingLogic;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.LauncherActivityCachingLogic;
-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;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.AppPairInfo;
@@ -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() {
@@ -209,7 +217,10 @@
                 mApp.getContext().getContentResolver(),
                 "launcher_broadcast_installed_apps",
                 /* def= */ 0);
-        if (launcherBroadcastInstalledApps == 1 && mIsRestoreFromBackup) {
+        boolean shouldAttachArchivingExtras = mIsRestoreFromBackup
+                && (launcherBroadcastInstalledApps == 1
+                        || Flags.enableFirstScreenBroadcastArchivingExtras());
+        if (shouldAttachArchivingExtras) {
             List<FirstScreenBroadcastModel> broadcastModels =
                     FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
                             mPmHelper,
@@ -243,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.
@@ -282,11 +293,6 @@
             }
             logASplit("loadAllApps");
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                logASplit("allAppsDelegateItems");
-            }
             verifyNotStopped();
             mLauncherBinder.bindAllApps();
             logASplit("bindAllApps");
@@ -295,13 +301,13 @@
             IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
             setIgnorePackages(updateHandler);
             updateHandler.updateIcons(allActivityList,
-                    LauncherActivityCachingLogic.newInstance(mApp.getContext()),
+                    LauncherActivityCachingLogic.INSTANCE,
                     mApp.getModel()::onPackageIconsUpdated);
             logASplit("update icon cache");
 
             verifyNotStopped();
             logASplit("save shortcuts in icon cache");
-            updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
+            updateHandler.updateIcons(allShortcuts, CacheableShortcutCachingLogic.INSTANCE,
                     mApp.getModel()::onPackageIconsUpdated);
 
             // Take a break
@@ -319,8 +325,10 @@
 
             verifyNotStopped();
             logASplit("save deep shortcuts in icon cache");
-            updateHandler.updateIcons(allDeepShortcuts,
-                    new ShortcutCachingLogic(), (pkgs, user) -> { });
+            updateHandler.updateIcons(
+                    convertShortcutsToCacheableShortcuts(allDeepShortcuts, allActivityList),
+                    CacheableShortcutCachingLogic.INSTANCE,
+                    (pkgs, user) -> { });
 
             // Take a break
             waitForIdle();
@@ -328,8 +336,15 @@
             verifyNotStopped();
 
             // fourth step
-            List<ComponentWithLabelAndIcon> allWidgetsList =
-                    mBgDataModel.widgetsModel.update(mApp, null);
+            WidgetsModel widgetsModel = mBgDataModel.widgetsModel;
+            if (enableTieredWidgetsByDefaultInPicker()) {
+                // Begin periodic refresh of filters
+                mWidgetsFilterDataProvider.initPeriodicDataRefresh(
+                        mApp.getModel()::onWidgetFiltersLoaded);
+                // And, update model with currently cached data.
+                widgetsModel.updateWidgetFilters(mWidgetsFilterDataProvider);
+            }
+            List<CachedObject> allWidgetsList = widgetsModel.update(mApp, /*packageUser=*/null);
             logASplit("load widgets");
 
             verifyNotStopped();
@@ -350,14 +365,8 @@
                 prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(true));
             }
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                logASplit("otherDelegateItems");
-                verifyNotStopped();
-            }
-
             updateHandler.updateIcons(allWidgetsList,
-                    new ComponentWithIconCachingLogic(mApp.getContext(), true),
+                    CachedObjectCachingLogic.INSTANCE,
                     mApp.getModel()::onWidgetLabelsUpdated);
             logASplit("save widgets in icon cache");
 
@@ -394,7 +403,7 @@
     }
 
     protected void loadWorkspace(
-            List<ShortcutInfo> allDeepShortcuts,
+            List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
             LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger
@@ -407,20 +416,13 @@
         }
         logASplit("loadWorkspace");
 
-        if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-            verifyNotStopped();
-            mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
-                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-            mModelDelegate.markActive();
-            logASplit("workspaceDelegateItems");
-        }
         mBgDataModel.isFirstPagePinnedItemEnabled = FeatureFlags.QSB_ON_FIRST_SCREEN
                 && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(
                 mApp.getContext()).getBoolean(SMARTSPACE_ON_HOME_SCREEN, true));
     }
 
     private void loadWorkspaceImpl(
-            List<ShortcutInfo> allDeepShortcuts,
+            List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
             @Nullable LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger) {
@@ -429,7 +431,15 @@
         final WidgetInflater widgetInflater = new WidgetInflater(context);
 
         ModelDbController dbController = mApp.getModel().getModelDbController();
-        dbController.tryMigrateDB(restoreEventLogger);
+        if (Flags.gridMigrationRefactor()) {
+            try {
+                dbController.attemptMigrateDb(restoreEventLogger);
+            } catch (Exception e) {
+                FileLog.e(TAG, "Failed to migrate grid", e);
+            }
+        } else {
+            dbController.tryMigrateDB(restoreEventLogger);
+        }
         Log.d(TAG, "loadWorkspace: loading default favorites");
         dbController.loadDefaultFavoritesIfNecessary();
 
@@ -476,14 +486,12 @@
                 IOUtils.closeSilently(c);
             }
 
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                mModelDelegate.markActive();
-            }
+            mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
+                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+            mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
+                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+            mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
+            mModelDelegate.markActive();
 
             // Break early if we've stopped loading
             if (mStopped) {
@@ -568,7 +576,7 @@
     private void processFolderItems() {
         // Sort the folder items, update ranks, and make sure all preview items are high res.
         List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
-                .stream().map(FolderGridOrganizer::new).toList();
+                .stream().map(FolderGridOrganizer::createFolderGridOrganizer).toList();
         for (CollectionInfo collection : mBgDataModel.collections) {
             if (!(collection instanceof FolderInfo folder)) {
                 continue;
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 7e1d40d..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;
@@ -83,8 +86,10 @@
 
 import org.xmlpull.v1.XmlPullParser;
 
+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
@@ -95,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;
 
@@ -104,16 +110,40 @@
         mContext = context;
     }
 
-    private synchronized void createDbIfNotExists() {
-        if (mOpenHelper == null) {
-            mOpenHelper = createDatabaseHelper(false /* forMigration */);
-            RestoreDbTask.restoreIfNeeded(mContext, this);
+    private void printDBs(String prefix) {
+        try {
+            File directory = new File(
+                    mContext.getDatabasePath(InvariantDeviceProfile.INSTANCE.get(mContext).dbFile)
+                            .getParent()
+            );
+            if (directory.exists()) {
+                for (File file : directory.listFiles()) {
+                    Log.d("b/353505773", prefix + "Database file: " + file.getName());
+                }
+            } else {
+                Log.d("b/353505773", prefix + "No files found in the database directory");
+            }
+        } catch (Exception e) {
+            Log.e("b/353505773", prefix + e.getMessage());
         }
     }
 
-    protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
+    private synchronized void createDbIfNotExists() {
+        if (mOpenHelper == null) {
+            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, 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 ? () -> { }
@@ -267,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();
@@ -282,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);
         }
     }
 
@@ -295,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;
@@ -527,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),
@@ -541,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 b12b2bc..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,9 +40,10 @@
  * 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")
         taskExecutor.accept(PackageUpdatedTask(OP_ADD, user, packageName))
     }
 
@@ -48,20 +54,20 @@
     override fun onPackageLoadingProgressChanged(
         packageName: String,
         user: UserHandle,
-        progress: Float
+        progress: Float,
     ) {
         taskExecutor.accept(PackageIncrementalDownloadUpdatedTask(packageName, user, progress))
     }
 
     override fun onPackageRemoved(packageName: String, user: UserHandle) {
-        FileLog.d(TAG, "package removed received $packageName")
+        FileLog.d(TAG, "onPackageRemoved triggered for packageName=$packageName, user=$user")
         taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, packageName))
     }
 
     override fun onPackagesAvailable(
         vararg packageNames: String,
         user: UserHandle,
-        replacing: Boolean
+        replacing: Boolean,
     ) {
         taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, *packageNames))
     }
@@ -73,7 +79,7 @@
     override fun onPackagesUnavailable(
         packageNames: Array<String>,
         user: UserHandle,
-        replacing: Boolean
+        replacing: Boolean,
     ) {
         if (!replacing) {
             taskExecutor.accept(PackageUpdatedTask(OP_UNAVAILABLE, user, *packageNames))
@@ -87,7 +93,7 @@
     override fun onShortcutsChanged(
         packageName: String,
         shortcuts: MutableList<ShortcutInfo>,
-        user: UserHandle
+        user: UserHandle,
     ) {
         taskExecutor.accept(ShortcutsChangedTask(packageName, shortcuts, user, true))
     }
@@ -97,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 266ed0c..fc53343 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -24,6 +24,7 @@
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder
 import java.util.Objects
 import java.util.concurrent.Executor
 import java.util.function.Predicate
@@ -34,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. */
@@ -78,8 +79,19 @@
     }
 
     fun bindUpdatedWidgets(dataModel: BgDataModel) {
-        val widgets = dataModel.widgetsModel.getWidgetsListForPicker(app.context)
-        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?) {
@@ -96,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/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 079987b..5464afe 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -109,7 +109,7 @@
         final IconCache iconCache = app.getIconCache();
 
         final String[] packages = mPackages;
-        final int N = packages.length;
+        final int packageCount = packages.length;
         final FlagOp flagOp;
         final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
         final Predicate<ItemInfo> matcher = mOp == OP_USER_AVAILABILITY_CHANGE
@@ -119,11 +119,12 @@
         final HashMap<String, List<LauncherActivityInfo>> activitiesLists = new HashMap<>();
         if (DEBUG) {
             Log.d(TAG, "Package updated: mOp=" + getOpString()
-                    + " packages=" + Arrays.toString(packages));
+                    + " packages=" + Arrays.toString(packages)
+                    + ", user=" + mUser);
         }
         switch (mOp) {
             case OP_ADD: {
-                for (int i = 0; i < N; i++) {
+                for (int i = 0; i < packageCount; i++) {
                     iconCache.updateIconsForPkg(packages[i], mUser);
                     if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
                         if (DEBUG) {
@@ -146,7 +147,7 @@
                             + " Look for earlier AllAppsList logs to find more information.");
                     removedComponents.add(a.componentName);
                 })) {
-                    for (int i = 0; i < N; i++) {
+                    for (int i = 0; i < packageCount; i++) {
                         iconCache.updateIconsForPkg(packages[i], mUser);
                         activitiesLists.put(packages[i],
                                 appsList.updatePackage(context, packages[i], mUser));
@@ -156,13 +157,13 @@
                 flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                 break;
             case OP_REMOVE: {
-                for (int i = 0; i < N; i++) {
+                for (int i = 0; i < packageCount; i++) {
                     iconCache.removeIconsForPkg(packages[i], mUser);
                 }
                 // Fall through
             }
             case OP_UNAVAILABLE:
-                for (int i = 0; i < N; i++) {
+                for (int i = 0; i < packageCount; i++) {
                     if (DEBUG) {
                         Log.d(TAG, getOpString() + ": removing package=" + packages[i]);
                     }
@@ -217,44 +218,44 @@
             // For system apps, package manager send OP_UPDATE when an app is enabled.
             final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE;
             synchronized (dataModel) {
-                dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+                dataModel.forAllWorkspaceItemInfos(mUser, itemInfo -> {
 
                     boolean infoUpdated = false;
                     boolean shortcutUpdated = false;
 
-                    ComponentName cn = si.getTargetComponent();
-                    if (cn != null && matcher.test(si)) {
+                    ComponentName cn = itemInfo.getTargetComponent();
+                    if (cn != null && matcher.test(itemInfo)) {
                         String packageName = cn.getPackageName();
 
-                        if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
-                            forceKeepShortcuts.add(si.id);
+                        if (itemInfo.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
+                            forceKeepShortcuts.add(itemInfo.id);
                             if (mOp == OP_REMOVE) {
                                 return;
                             }
                         }
 
-                        if (si.isPromise() && isNewApkAvailable) {
+                        if (itemInfo.isPromise() && isNewApkAvailable) {
                             boolean isTargetValid = !cn.getClassName().equals(
                                     IconCache.EMPTY_CLASS_NAME);
-                            if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                            if (itemInfo.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                                 List<ShortcutInfo> shortcut =
                                         new ShortcutRequest(context, mUser)
                                                 .forPackage(cn.getPackageName(),
-                                                        si.getDeepShortcutId())
+                                                        itemInfo.getDeepShortcutId())
                                                 .query(ShortcutRequest.PINNED);
                                 if (shortcut.isEmpty()) {
                                     isTargetValid = false;
                                     if (DEBUG) {
                                         Log.d(TAG, "Pinned Shortcut not found for updated"
-                                                + " package=" + si.getTargetPackage());
+                                                + " package=" + itemInfo.getTargetPackage());
                                     }
                                 } else {
                                     if (DEBUG) {
                                         Log.d(TAG, "Found pinned shortcut for updated"
-                                                + " package=" + si.getTargetPackage()
+                                                + " package=" + itemInfo.getTargetPackage()
                                                 + ", isTargetValid=" + isTargetValid);
                                     }
-                                    si.updateFromDeepShortcutInfo(shortcut.get(0), context);
+                                    itemInfo.updateFromDeepShortcutInfo(shortcut.get(0), context);
                                     infoUpdated = true;
                                 }
                             } else if (isTargetValid) {
@@ -262,39 +263,39 @@
                                         .isActivityEnabled(cn, mUser);
                             }
 
-                            if (!isTargetValid && (si.hasStatusFlag(
+                            if (!isTargetValid && (itemInfo.hasStatusFlag(
                                     FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)
-                                    || si.isArchived())) {
-                                if (updateWorkspaceItemIntent(context, si, packageName)) {
+                                    || itemInfo.isArchived())) {
+                                if (updateWorkspaceItemIntent(context, itemInfo, packageName)) {
                                     infoUpdated = true;
-                                } else if (si.hasPromiseIconUi()) {
-                                    removedShortcuts.add(si.id);
+                                } else if (itemInfo.hasPromiseIconUi()) {
+                                    removedShortcuts.add(itemInfo.id);
                                     if (DEBUG) {
                                         FileLog.w(TAG, "Removing restored shortcut promise icon"
                                                 + " that no longer points to valid component."
-                                                + " id=" + si.id
-                                                + ", package=" + si.getTargetPackage()
-                                                + ", status=" + si.status
-                                                + ", isArchived=" + si.isArchived());
+                                                + " id=" + itemInfo.id
+                                                + ", package=" + itemInfo.getTargetPackage()
+                                                + ", status=" + itemInfo.status
+                                                + ", isArchived=" + itemInfo.isArchived());
                                     }
                                     return;
                                 }
                             } else if (!isTargetValid) {
-                                removedShortcuts.add(si.id);
+                                removedShortcuts.add(itemInfo.id);
                                 if (DEBUG) {
                                     FileLog.w(TAG, "Removing shortcut that no longer points to"
                                             + " valid component."
-                                            + " id=" + si.id
-                                            + " package=" + si.getTargetPackage()
-                                            + " status=" + si.status);
+                                            + " id=" + itemInfo.id
+                                            + " package=" + itemInfo.getTargetPackage()
+                                            + " status=" + itemInfo.status);
                                 }
                                 return;
                             } else {
-                                si.status = WorkspaceItemInfo.DEFAULT;
+                                itemInfo.status = WorkspaceItemInfo.DEFAULT;
                                 infoUpdated = true;
                             }
                         } else if (isNewApkAvailable && removedComponents.contains(cn)) {
-                            if (updateWorkspaceItemIntent(context, si, packageName)) {
+                            if (updateWorkspaceItemIntent(context, itemInfo, packageName)) {
                                 infoUpdated = true;
                             }
                         }
@@ -304,7 +305,7 @@
                                     packageName);
                             // TODO: See if we can migrate this to
                             //  AppInfo#updateRuntimeFlagsForActivityTarget
-                            si.setProgressLevel(
+                            itemInfo.setProgressLevel(
                                     activities == null || activities.isEmpty()
                                             ? 100
                                             : PackageManagerHelper.getLoadingProgress(
@@ -313,42 +314,42 @@
                             // In case an app is archived, we need to make sure that archived state
                             // in WorkspaceItemInfo is refreshed.
                             if (Flags.enableSupportForArchiving() && !activities.isEmpty()) {
-                                boolean newArchivalState = activities.get(
-                                        0).getActivityInfo().isArchived;
-                                if (newArchivalState != si.isArchived()) {
-                                    si.runtimeStatusFlags ^= FLAG_ARCHIVED;
+                                boolean newArchivalState = activities.get(0)
+                                        .getActivityInfo().isArchived;
+                                if (newArchivalState != itemInfo.isArchived()) {
+                                    itemInfo.runtimeStatusFlags ^= FLAG_ARCHIVED;
                                     infoUpdated = true;
                                 }
                             }
-                            if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                            if (itemInfo.itemType == Favorites.ITEM_TYPE_APPLICATION) {
                                 if (activities != null && !activities.isEmpty()) {
-                                    si.setNonResizeable(ApiWrapper.INSTANCE.get(context)
+                                    itemInfo.setNonResizeable(ApiWrapper.INSTANCE.get(context)
                                             .isNonResizeableActivity(activities.get(0)));
                                 }
-                                iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+                                iconCache.getTitleAndIcon(itemInfo, itemInfo.usingLowResIcon());
                                 infoUpdated = true;
                             }
                         }
 
-                        int oldRuntimeFlags = si.runtimeStatusFlags;
-                        si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags);
-                        if (si.runtimeStatusFlags != oldRuntimeFlags) {
+                        int oldRuntimeFlags = itemInfo.runtimeStatusFlags;
+                        itemInfo.runtimeStatusFlags = flagOp.apply(itemInfo.runtimeStatusFlags);
+                        if (itemInfo.runtimeStatusFlags != oldRuntimeFlags) {
                             shortcutUpdated = true;
                         }
                     }
 
                     if (infoUpdated || shortcutUpdated) {
-                        updatedWorkspaceItems.add(si);
+                        updatedWorkspaceItems.add(itemInfo);
                     }
-                    if (infoUpdated && si.id != ItemInfo.NO_ID) {
-                        taskController.getModelWriter().updateItemInDatabase(si);
+                    if (infoUpdated && itemInfo.id != ItemInfo.NO_ID) {
+                        taskController.getModelWriter().updateItemInDatabase(itemInfo);
                     }
                 });
 
                 for (LauncherAppWidgetInfo widgetInfo : dataModel.appWidgets) {
                     if (mUser.equals(widgetInfo.user)
                             && widgetInfo.hasRestoreFlag(
-                                    LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+                            LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
                             && packageSet.contains(widgetInfo.providerName.getPackageName())) {
                         widgetInfo.restoreStatus &=
                                 ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
@@ -391,7 +392,7 @@
         } else if (mOp == OP_UPDATE) {
             // Mark disabled packages in the broadcast to be removed
             final LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
-            for (int i=0; i<N; i++) {
+            for (int i = 0; i < packageCount; i++) {
                 if (!launcherApps.isPackageEnabled(packages[i], mUser)) {
                     if (DEBUG) {
                         Log.d(TAG, "OP_UPDATE:"
@@ -423,7 +424,7 @@
         if (mOp == OP_ADD) {
             // Load widgets for the new package. Changes due to app updates are handled through
             // AppWidgetHost events, this is just to initialize the long-press options.
-            for (int i = 0; i < N; i++) {
+            for (int i = 0; i < packageCount; i++) {
                 dataModel.widgetsModel.update(app, new PackageUserKey(packages[i], mUser));
             }
             taskController.bindUpdatedWidgets(dataModel);
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 3f88717..e757a68 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -4,12 +4,9 @@
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX;
 
-import static com.android.launcher3.Utilities.ATLEAST_S;
-
 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;
@@ -48,7 +45,7 @@
         super(info.provider, info.getProfile());
 
         label = iconCache.getTitleNoCache(info);
-        description = ATLEAST_S ? info.loadDescription(context) : null;
+        description = info.loadDescription(context);
         widgetInfo = info;
         activityInfo = null;
 
@@ -77,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;
@@ -107,7 +104,7 @@
     /** Returns whether this {@link WidgetItem} has a preview layout that can be used. */
     @SuppressLint("NewApi") // Already added API check.
     public boolean hasPreviewLayout() {
-        return ATLEAST_S && widgetInfo != null && widgetInfo.previewLayout != Resources.ID_NULL;
+        return widgetInfo != null && widgetInfo.previewLayout != Resources.ID_NULL;
     }
 
     /** Returns whether this {@link WidgetItem} is for a shortcut rather than an app widget. */
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 454ae96..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;
 
@@ -26,10 +27,9 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.AlphabeticIndexCompat;
 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;
@@ -39,9 +39,6 @@
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 import com.android.launcher3.widget.WidgetSections;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 import com.android.wm.shell.Flags;
 
 import java.util.ArrayList;
@@ -54,7 +51,9 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * Widgets data model that is used by the adapters of the widget views and controllers.
@@ -67,104 +66,82 @@
     private static final boolean DEBUG = false;
 
     /* Map of widgets and shortcuts that are tracked per package. */
-    private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
+    private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
+    @Nullable private Predicate<WidgetItem> mDefaultWidgetsFilter = null;
+    @Nullable private Predicate<WidgetItem> mPredictedWidgetsFilter = null;
 
     /**
-     * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All
-     * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall
-     * list of {@link WidgetsListBaseEntry}s is not sorted.
-     *
-     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
+     * Returns all widgets keyed by their component key.
      */
-    public synchronized ArrayList<WidgetsListBaseEntry> getFilteredWidgetsListForPicker(
-            Context context,
-            Predicate<WidgetItem> widgetItemFilter) {
-        if (!WIDGETS_ENABLED) {
-            return new ArrayList<>();
-        }
-        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
-        AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
-
-        for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
-            PackageItemInfo pkgItem = entry.getKey();
-            List<WidgetItem> widgetItems = entry.getValue()
-                    .stream()
-                    .filter(widgetItemFilter).toList();
-            if (!widgetItems.isEmpty()) {
-                String sectionName = (pkgItem.title == null) ? "" :
-                        indexer.computeSectionName(pkgItem.title);
-                result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems));
-                result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
-     * are sorted (based on label and user), but the overall list of
-     * {@link WidgetsListBaseEntry}s is not sorted.
-     *
-     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
-     */
-    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
-        // return all items
-        return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true);
-    }
-
-    /** Returns a mapping of packages to their widgets without static shortcuts. */
-    public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
+    public synchronized Map<ComponentKey, WidgetItem> getWidgetsByComponentKey() {
         if (!WIDGETS_ENABLED) {
             return Collections.emptyMap();
         }
-        Map<PackageUserKey, List<WidgetItem>> packagesToWidgets = new HashMap<>();
-        mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> {
-            List<WidgetItem> widgets = widgetsAndShortcuts.stream()
-                    .filter(item -> item.widgetInfo != null)
-                    .collect(toList());
-            if (widgets.size() > 0) {
-                packagesToWidgets.put(
-                        new PackageUserKey(packageItemInfo.packageName, packageItemInfo.user),
-                        widgets);
-            }
-        });
-        return packagesToWidgets;
+        return mWidgetsByPackageItem.values().stream()
+                .flatMap(Collection::stream).distinct()
+                .collect(Collectors.toMap(
+                        widget -> new ComponentKey(widget.componentName, widget.user),
+                        Function.identity()
+                ));
     }
 
     /**
-     * Returns a map of widget component keys to corresponding widget items. Excludes the
-     * shortcuts.
+     * Returns widgets grouped by the package item that they should belong to.
      */
-    public synchronized Map<ComponentKey, WidgetItem> getAllWidgetComponentsWithoutShortcuts() {
+    public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItem() {
         if (!WIDGETS_ENABLED) {
             return Collections.emptyMap();
         }
-        Map<ComponentKey, WidgetItem> widgetsMap = new HashMap<>();
-        mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) ->
-                widgetsAndShortcuts.stream().filter(item -> item.widgetInfo != null).forEach(
-                        item -> widgetsMap.put(new ComponentKey(item.componentName, item.user),
-                                item)));
-        return widgetsMap;
+        return new HashMap<>(mWidgetsByPackageItem);
+    }
+
+    /**
+     * 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)) {
@@ -180,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);
@@ -210,14 +187,14 @@
 
         if (packageUser == null) {
             // Clear the list if this is an update on all widgets and shortcuts.
-            mWidgetsList.clear();
+            mWidgetsByPackageItem.clear();
         } else {
             // Otherwise, only clear the widgets and shortcuts for the changed package.
-            mWidgetsList.remove(packageItemInfoCache.getOrCreate(packageUser));
+            mWidgetsByPackageItem.remove(packageItemInfoCache.getOrCreate(packageUser));
         }
 
         // add and update.
-        mWidgetsList.putAll(rawWidgetsShortcuts.stream()
+        mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream()
                 .filter(new WidgetValidityCheck(app))
                 .filter(new WidgetFlagCheck())
                 .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
@@ -237,7 +214,7 @@
             return;
         }
         WidgetManagerHelper widgetManager = new WidgetManagerHelper(app.getContext());
-        for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
+        for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsByPackageItem.entrySet()) {
             if (packageNames.contains(entry.getKey().packageName)) {
                 List<WidgetItem> items = entry.getValue();
                 int count = items.size();
@@ -245,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(),
@@ -258,50 +234,6 @@
         }
     }
 
-    private PackageItemInfo createPackageItemInfo(
-            ComponentName providerName,
-            UserHandle user,
-            int category
-    ) {
-        if (category == NO_CATEGORY) {
-            return new PackageItemInfo(providerName.getPackageName(), user);
-        } else {
-            return new PackageItemInfo("" , category, user);
-        }
-    }
-
-    private IntSet getCategories(ComponentName providerName, Context context) {
-        IntSet categories = WidgetSections.getWidgetsToCategory(context).get(providerName);
-        if (categories != null) {
-            return categories;
-        }
-        categories = new IntSet();
-        categories.add(NO_CATEGORY);
-        return categories;
-    }
-
-    public WidgetItem getWidgetProviderInfoByProviderName(
-            ComponentName providerName, UserHandle user, Context context) {
-        if (!WIDGETS_ENABLED) {
-            return null;
-        }
-        IntSet categories = getCategories(providerName, context);
-
-        // Checking if we have a provider in any of the categories.
-        for (Integer category: categories) {
-            PackageItemInfo key = createPackageItemInfo(providerName, user, category);
-            List<WidgetItem> widgets = mWidgetsList.get(key);
-            if (widgets != null) {
-                return widgets.stream().filter(
-                                item -> item.componentName.equals(providerName)
-                        )
-                        .findFirst()
-                        .orElse(null);
-            }
-        }
-        return null;
-    }
-
     /** Returns {@link PackageItemInfo} of a pending widget. */
     public static PackageItemInfo newPendingItemInfo(Context context, ComponentName provider,
             UserHandle user) {
@@ -402,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 90e47d6..c02336e 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -30,9 +30,9 @@
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
-import com.android.launcher3.Utilities
 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
@@ -45,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
@@ -76,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
@@ -97,7 +98,7 @@
                 // User has been deleted, remove the item.
                 c.markDeleted(
                     "User has been deleted for item id=${c.id}",
-                    RestoreError.PROFILE_DELETED
+                    RestoreError.PROFILE_DELETED,
                 )
                 return
             }
@@ -153,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
@@ -168,7 +170,7 @@
                 FileLog.d(
                     TAG,
                     "Activity not enabled for id=${c.id}, component=$cn, user=${c.user}." +
-                        " Will attempt to find fallback Activity for targetPkg=$targetPkg."
+                        " Will attempt to find fallback Activity for targetPkg=$targetPkg.",
                 )
                 intent = pmHelper.getAppLaunchIntent(targetPkg, c.user)
                 if (intent != null) {
@@ -178,7 +180,7 @@
                     c.markDeleted(
                         "No Activities found for id=${c.id}, targetPkg=$targetPkg, component=$cn." +
                             " Unable to create launch Intent.",
-                        RestoreError.MISSING_INFO
+                        RestoreError.MISSING_INFO,
                     )
                     return
                 }
@@ -213,13 +215,13 @@
                             else -> {
                                 c.markDeleted(
                                     "removing app that is not restored and not installing. package: $targetPkg",
-                                    RestoreError.APP_NOT_INSTALLED
+                                    RestoreError.APP_NOT_INSTALLED,
                                 )
                                 return
                             }
                         }
                     }
-                    pmHelper.isAppOnSdcard(targetPkg, c.user) -> {
+                    appInfoWrapper.isOnSdCard() -> {
                         // Package is present but not available.
                         disabledState =
                             disabledState or WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE
@@ -238,7 +240,7 @@
                         // Do not wait for external media load anymore.
                         c.markDeleted(
                             "Invalid package removed: $targetPkg",
-                            RestoreError.APP_NOT_INSTALLED
+                            RestoreError.APP_NOT_INSTALLED,
                         )
                         return
                     }
@@ -270,20 +272,21 @@
                         // The shortcut is no longer valid.
                         c.markDeleted(
                             "Pinned shortcut not found from request. package=${key.packageName}, user=${c.user}",
-                            RestoreError.SHORTCUT_NOT_FOUND
+                            RestoreError.SHORTCUT_NOT_FOUND,
                         )
                         return
                     }
                     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()
@@ -295,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
@@ -326,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
             }
@@ -337,7 +340,7 @@
                     activityInfo,
                     userCache.getUserInfo(c.user),
                     ApiWrapper.INSTANCE[app.context],
-                    pmHelper
+                    pmHelper,
                 )
             }
             if (
@@ -445,7 +448,7 @@
                     ", id=${c.id}," +
                     ", appWidgetId=${c.appWidgetId}," +
                     ", component=${component}",
-                RestoreError.INVALID_LOCATION
+                RestoreError.INVALID_LOCATION,
             )
             return
         }
@@ -456,7 +459,7 @@
                     ", appWidgetId=${c.appWidgetId}," +
                     ", component=${component}," +
                     ", container=${c.container}",
-                RestoreError.INVALID_LOCATION
+                RestoreError.INVALID_LOCATION,
             )
             return
         }
@@ -470,7 +473,7 @@
             TAG,
             "processWidget: id=${c.id}" +
                 ", appWidgetId=${c.appWidgetId}" +
-                ", inflationResult=$inflationResult"
+                ", inflationResult=$inflationResult",
         )
         when (inflationResult.type) {
             WidgetInflater.TYPE_DELETE -> {
@@ -487,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(
@@ -496,7 +500,7 @@
                             ", appWidgetId=${c.appWidgetId}" +
                             ", component=${component}" +
                             ", restoreFlag:=${c.restoreFlag}",
-                        RestoreError.APP_NOT_INSTALLED
+                        RestoreError.APP_NOT_INSTALLED,
                     )
                     return
                 } else if (
@@ -512,7 +516,7 @@
                     WidgetsModel.newPendingItemInfo(
                         app.context,
                         appWidgetInfo.providerName,
-                        appWidgetInfo.user
+                        appWidgetInfo.user,
                     )
                 iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false)
             }
@@ -522,7 +526,7 @@
                     lapi,
                     app.context,
                     appWidgetInfo.spanX,
-                    appWidgetInfo.spanY
+                    appWidgetInfo.spanY,
                 )
         }
 
@@ -541,7 +545,7 @@
                     " processWidget: Widget ${lapi.component} minSizes not met: span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY} minSpan=${lapi.minSpanX}x${lapi.minSpanY}," +
                         " id: ${c.id}," +
                         " appWidgetId: ${c.appWidgetId}," +
-                        " component=${component}"
+                        " component=${component}",
                 )
                 logWidgetInfo(app.invariantDeviceProfile, lapi)
             }
@@ -554,7 +558,7 @@
 
         private fun logWidgetInfo(
             idp: InvariantDeviceProfile,
-            widgetProviderInfo: LauncherAppWidgetProviderInfo
+            widgetProviderInfo: LauncherAppWidgetProviderInfo,
         ) {
             val cellSize = Point()
             for (deviceProfile in idp.supportedProfiles) {
@@ -565,7 +569,7 @@
                         " available height: ${deviceProfile.availableHeightPx}," +
                         " cellLayoutBorderSpacePx Horizontal: ${deviceProfile.cellLayoutBorderSpacePx.x}," +
                         " cellLayoutBorderSpacePx Vertical: ${deviceProfile.cellLayoutBorderSpacePx.y}," +
-                        " cellSize: $cellSize"
+                        " cellSize: $cellSize",
                 )
             }
             val widgetDimension = StringBuilder()
@@ -583,21 +587,19 @@
                 .append("defaultHeight: ")
                 .append(widgetProviderInfo.minHeight)
                 .append("\n")
-            if (Utilities.ATLEAST_S) {
-                widgetDimension
-                    .append("targetCellWidth: ")
-                    .append(widgetProviderInfo.targetCellWidth)
-                    .append("\n")
-                    .append("targetCellHeight: ")
-                    .append(widgetProviderInfo.targetCellHeight)
-                    .append("\n")
-                    .append("maxResizeWidth: ")
-                    .append(widgetProviderInfo.maxResizeWidth)
-                    .append("\n")
-                    .append("maxResizeHeight: ")
-                    .append(widgetProviderInfo.maxResizeHeight)
-                    .append("\n")
-            }
+            widgetDimension
+                .append("targetCellWidth: ")
+                .append(widgetProviderInfo.targetCellWidth)
+                .append("\n")
+                .append("targetCellHeight: ")
+                .append(widgetProviderInfo.targetCellHeight)
+                .append("\n")
+                .append("maxResizeWidth: ")
+                .append(widgetProviderInfo.maxResizeWidth)
+                .append("\n")
+                .append("maxResizeHeight: ")
+                .append(widgetProviderInfo.maxResizeHeight)
+                .append("\n")
             FileLog.d(TAG, widgetDimension.toString())
         }
     }
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 b82d0a0..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;
@@ -51,6 +52,7 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.AllAppsContainer;
+import com.android.launcher3.logger.LauncherAtom.Attribute;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import com.android.launcher3.logger.LauncherAtom.PredictionContainer;
 import com.android.launcher3.logger.LauncherAtom.SettingsContainer;
@@ -67,6 +69,9 @@
 import com.android.launcher3.util.UserIconInfo;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
 
 /**
@@ -77,8 +82,6 @@
 
     public static final boolean DEBUG = false;
     public static final int NO_ID = -1;
-    // An id that doesn't match any item, including predicted apps with have an id=NO_ID
-    public static final int NO_MATCHING_ID = Integer.MIN_VALUE;
 
     /** Hidden field Settings.Secure.NAV_BAR_KIDS_MODE */
     private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor("nav_bar_kids_mode");
@@ -187,6 +190,12 @@
     @NonNull
     public UserHandle user;
 
+    @NonNull
+    private ExtendedContainers mExtendedContainers = ExtendedContainers.getDefaultInstance();
+
+    @NonNull
+    private List<Attribute> mAttributeList = Collections.EMPTY_LIST;
+
     public ItemInfo() {
         user = Process.myUserHandle();
     }
@@ -344,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:
@@ -426,13 +435,14 @@
     }
 
     @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);
+        itemBuilder.addAllItemAttributes(mAttributeList);
         return itemBuilder;
     }
 
@@ -491,7 +501,7 @@
             default:
                 if (container <= EXTENDED_CONTAINERS) {
                     return ContainerInfo.newBuilder()
-                            .setExtendedContainers(getExtendedContainer())
+                            .setExtendedContainers(mExtendedContainers)
                             .build();
                 }
         }
@@ -499,12 +509,21 @@
     }
 
     /**
-     * Returns non-AOSP container wrapped by {@link ExtendedContainers} object. Should be overridden
-     * by build variants.
+     * Sets extra container info wrapped by {@link ExtendedContainers} object.
      */
-    @NonNull
-    protected ExtendedContainers getExtendedContainer() {
-        return ExtendedContainers.getDefaultInstance();
+    public void setExtendedContainers(@NonNull ExtendedContainers extendedContainers) {
+        mExtendedContainers = extendedContainers;
+    }
+
+    /**
+     * Adds extra attributes to be added during logs
+     */
+    public void addLogAttributes(List<LauncherAtom.Attribute> attributeList) {
+        if (mAttributeList.isEmpty()) {
+            mAttributeList = new ArrayList<>(attributeList);
+        } else {
+            mAttributeList.addAll(attributeList);
+        }
     }
 
     /**
@@ -525,6 +544,14 @@
         this.title = title;
     }
 
+    /**
+     * Returns a string ID that is stable for a user session, but may not be persisted
+     */
+    @Nullable
+    public Object getStableId() {
+        return getComponentKey();
+    }
+
     private int getUserType(UserIconInfo info) {
         if (info == null) {
             return SysUiStatsLog.LAUNCHER_UICHANGED__USER_TYPE__TYPE_UNKNOWN;
diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index f4dda55..7569ed5 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -21,10 +21,10 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PIN_WIDGETS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
-import static com.android.launcher3.Utilities.ATLEAST_S;
 
 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;
@@ -233,16 +233,16 @@
         if (providerInfo.isConfigurationOptional()) {
             widgetFeatures |= FEATURE_OPTIONAL_CONFIGURATION;
         }
-        if (ATLEAST_S && providerInfo.previewLayout != Resources.ID_NULL) {
+        if (providerInfo.previewLayout != Resources.ID_NULL) {
             widgetFeatures |= FEATURE_PREVIEW_LAYOUT;
         }
-        if (ATLEAST_S && providerInfo.targetCellWidth > 0 || providerInfo.targetCellHeight > 0) {
+        if (providerInfo.targetCellWidth > 0 || providerInfo.targetCellHeight > 0) {
             widgetFeatures |= FEATURE_TARGET_CELL_SIZE;
         }
         if (providerInfo.minResizeWidth > 0 || providerInfo.minResizeHeight > 0) {
             widgetFeatures |= FEATURE_MIN_SIZE;
         }
-        if (ATLEAST_S && providerInfo.maxResizeWidth > 0 || providerInfo.maxResizeHeight > 0) {
+        if (providerInfo.maxResizeWidth > 0 || providerInfo.maxResizeHeight > 0) {
             widgetFeatures |= FEATURE_MAX_SIZE;
         }
         if (hostView instanceof LauncherAppWidgetHostView &&
@@ -271,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 40e3813..9af61f0 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -25,6 +25,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherSettings;
@@ -97,6 +98,8 @@
 
     public int options;
 
+    @Nullable
+    private ShortcutInfo mShortcutInfo = null;
 
     public WorkspaceItemInfo() {
         itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
@@ -175,6 +178,9 @@
 
     public void updateFromDeepShortcutInfo(@NonNull final ShortcutInfo shortcutInfo,
             @NonNull final Context context) {
+        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+            mShortcutInfo = shortcutInfo;
+        }
         // {@link ShortcutInfo#getActivity} can change during an update. Recreate the intent
         intent = ShortcutKey.makeIntent(shortcutInfo);
         title = shortcutInfo.getShortLabel();
@@ -183,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 {
@@ -204,6 +216,11 @@
             : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);
     }
 
+    @Nullable
+    public ShortcutInfo getDeepShortcutInfo() {
+        return mShortcutInfo;
+    }
+
     /**
      * {@code true} if the shortcut is disabled due to its app being a lower version.
      */
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index e44ea1d..a691e45 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -43,6 +43,7 @@
 import android.view.animation.OvershootInterpolator;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
@@ -131,7 +132,8 @@
     private float mCurrentPosition;
     private float mFinalPosition;
     private boolean mIsScrollPaused;
-    private boolean mIsTwoPanels;
+    @VisibleForTesting
+    boolean mIsTwoPanels;
     private ObjectAnimator mAnimator;
     private @Nullable ObjectAnimator mAlphaAnimator;
 
@@ -477,6 +479,21 @@
         return sTempRect;
     }
 
+    @VisibleForTesting
+    int getActivePage() {
+        return mActivePage;
+    }
+
+    @VisibleForTesting
+    int getNumPages() {
+        return mNumPages;
+    }
+
+    @VisibleForTesting
+    float getCurrentPosition() {
+        return mCurrentPosition;
+    }
+
     private class MyOutlineProver extends ViewOutlineProvider {
 
         @Override
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
deleted file mode 100644
index bde4e52..0000000
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ /dev/null
@@ -1,265 +0,0 @@
-package com.android.launcher3.pageindicators;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.AttributeSet;
-import android.util.Property;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Themes;
-
-/**
- * A PageIndicator that briefly shows a fraction of a line when moving between pages
- *
- * The fraction is 1 / number of pages and the position is based on the progress of the page scroll.
- */
-public class WorkspacePageIndicator extends View implements Insettable, PageIndicator {
-
-    private static final int LINE_ANIMATE_DURATION = ViewConfiguration.getScrollBarFadeDuration();
-    private static final int LINE_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay();
-    public static final int WHITE_ALPHA = (int) (0.70f * 255);
-    public static final int BLACK_ALPHA = (int) (0.65f * 255);
-
-    private static final int LINE_ALPHA_ANIMATOR_INDEX = 0;
-    private static final int NUM_PAGES_ANIMATOR_INDEX = 1;
-    private static final int TOTAL_SCROLL_ANIMATOR_INDEX = 2;
-    private static final int ANIMATOR_COUNT = 3;
-
-    private ValueAnimator[] mAnimators = new ValueAnimator[ANIMATOR_COUNT];
-
-    private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper());
-    private final Launcher mLauncher;
-
-    private boolean mShouldAutoHide = true;
-
-    // The alpha of the line when it is showing.
-    private int mActiveAlpha = 0;
-    // The alpha that the line is being animated to or already at (either 0 or mActiveAlpha).
-    private int mToAlpha;
-    // A float value representing the number of pages, to allow for an animation when it changes.
-    private float mNumPagesFloat;
-    private int mCurrentScroll;
-    private int mTotalScroll;
-    private Paint mLinePaint;
-    private final int mLineHeight;
-
-    private static final Property<WorkspacePageIndicator, Integer> PAINT_ALPHA
-            = new Property<WorkspacePageIndicator, Integer>(Integer.class, "paint_alpha") {
-        @Override
-        public Integer get(WorkspacePageIndicator obj) {
-            return obj.mLinePaint.getAlpha();
-        }
-
-        @Override
-        public void set(WorkspacePageIndicator obj, Integer alpha) {
-            obj.mLinePaint.setAlpha(alpha);
-            obj.invalidate();
-        }
-    };
-
-    private static final Property<WorkspacePageIndicator, Float> NUM_PAGES
-            = new Property<WorkspacePageIndicator, Float>(Float.class, "num_pages") {
-        @Override
-        public Float get(WorkspacePageIndicator obj) {
-            return obj.mNumPagesFloat;
-        }
-
-        @Override
-        public void set(WorkspacePageIndicator obj, Float numPages) {
-            obj.mNumPagesFloat = numPages;
-            obj.invalidate();
-        }
-    };
-
-    private static final Property<WorkspacePageIndicator, Integer> TOTAL_SCROLL
-            = new Property<WorkspacePageIndicator, Integer>(Integer.class, "total_scroll") {
-        @Override
-        public Integer get(WorkspacePageIndicator obj) {
-            return obj.mTotalScroll;
-        }
-
-        @Override
-        public void set(WorkspacePageIndicator obj, Integer totalScroll) {
-            obj.mTotalScroll = totalScroll;
-            obj.invalidate();
-        }
-    };
-
-    private Runnable mHideLineRunnable = () -> animateLineToAlpha(0);
-
-    public WorkspacePageIndicator(Context context) {
-        this(context, null);
-    }
-
-    public WorkspacePageIndicator(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        Resources res = context.getResources();
-        mLinePaint = new Paint();
-        mLinePaint.setAlpha(0);
-
-        mLauncher = Launcher.getLauncher(context);
-        mLineHeight = res.getDimensionPixelSize(R.dimen.workspace_page_indicator_line_height);
-
-        boolean darkText = Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText);
-        mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA;
-        mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE);
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        if (mTotalScroll == 0 || mNumPagesFloat == 0) {
-            return;
-        }
-
-        // Compute and draw line rect.
-        float progress = Utilities.boundToRange(((float) mCurrentScroll) / mTotalScroll, 0f, 1f);
-        int availableWidth = getWidth();
-        int lineWidth = (int) (availableWidth / mNumPagesFloat);
-        int lineLeft = (int) (progress * (availableWidth - lineWidth));
-        int lineRight = lineLeft + lineWidth;
-
-        canvas.drawRoundRect(lineLeft, getHeight() / 2 - mLineHeight / 2, lineRight,
-                getHeight() / 2 + mLineHeight / 2, mLineHeight, mLineHeight, mLinePaint);
-    }
-
-    @Override
-    public void setScroll(int currentScroll, int totalScroll) {
-        if (getAlpha() == 0) {
-            return;
-        }
-        animateLineToAlpha(mActiveAlpha);
-
-        mCurrentScroll = currentScroll;
-        if (mTotalScroll == 0) {
-            mTotalScroll = totalScroll;
-        } else if (mTotalScroll != totalScroll) {
-            animateToTotalScroll(totalScroll);
-        } else {
-            invalidate();
-        }
-
-        if (mShouldAutoHide) {
-            hideAfterDelay();
-        }
-    }
-
-    private void hideAfterDelay() {
-        mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
-        mDelayedLineFadeHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY);
-    }
-
-    @Override
-    public void setActiveMarker(int activePage) { }
-
-    @Override
-    public void setMarkersCount(int numMarkers) {
-        if (Float.compare(numMarkers, mNumPagesFloat) != 0) {
-            setupAndRunAnimation(ObjectAnimator.ofFloat(this, NUM_PAGES, numMarkers),
-                    NUM_PAGES_ANIMATOR_INDEX);
-        } else {
-            if (mAnimators[NUM_PAGES_ANIMATOR_INDEX] != null) {
-                mAnimators[NUM_PAGES_ANIMATOR_INDEX].cancel();
-                mAnimators[NUM_PAGES_ANIMATOR_INDEX] = null;
-            }
-        }
-    }
-
-    @Override
-    public void setShouldAutoHide(boolean shouldAutoHide) {
-        mShouldAutoHide = shouldAutoHide;
-        if (shouldAutoHide && mLinePaint.getAlpha() > 0) {
-            hideAfterDelay();
-        } else if (!shouldAutoHide) {
-            mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
-        }
-    }
-
-    private void animateLineToAlpha(int alpha) {
-        if (alpha == mToAlpha) {
-            // Ignore the new animation if it is going to the same alpha as the current animation.
-            return;
-        }
-        mToAlpha = alpha;
-        setupAndRunAnimation(ObjectAnimator.ofInt(this, PAINT_ALPHA, alpha),
-                LINE_ALPHA_ANIMATOR_INDEX);
-    }
-
-    private void animateToTotalScroll(int totalScroll) {
-        setupAndRunAnimation(ObjectAnimator.ofInt(this, TOTAL_SCROLL, totalScroll),
-                TOTAL_SCROLL_ANIMATOR_INDEX);
-    }
-
-    /**
-     * Starts the given animator and stores it in the provided index in {@link #mAnimators} until
-     * the animation ends.
-     *
-     * If an animator is already at the index (i.e. it is already playing), it is canceled and
-     * replaced with the new animator.
-     */
-    private void setupAndRunAnimation(ValueAnimator animator, final int animatorIndex) {
-        if (mAnimators[animatorIndex] != null) {
-            mAnimators[animatorIndex].cancel();
-        }
-        mAnimators[animatorIndex] = animator;
-        mAnimators[animatorIndex].addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimators[animatorIndex] = null;
-            }
-        });
-        mAnimators[animatorIndex].setDuration(LINE_ANIMATE_DURATION);
-        mAnimators[animatorIndex].start();
-    }
-
-    /**
-     * Pauses all currently running animations.
-     */
-    @Override
-    public void pauseAnimations() {
-        for (int i = 0; i < ANIMATOR_COUNT; i++) {
-            if (mAnimators[i] != null) {
-                mAnimators[i].pause();
-            }
-        }
-    }
-
-    /**
-     * Force-ends all currently running or paused animations.
-     */
-    @Override
-    public void skipAnimationsToEnd() {
-        for (int i = 0; i < ANIMATOR_COUNT; i++) {
-            if (mAnimators[i] != null) {
-                mAnimators[i].end();
-            }
-        }
-    }
-
-    /**
-     * We need to override setInsets to prevent InsettableFrameLayout from applying different
-     * margins on the page indicator.
-     */
-    @Override
-    public void setInsets(Rect insets) {
-    }
-}
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 24d58f3..856c294 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -25,6 +25,7 @@
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.os.Build;
 import android.os.UserHandle;
+import android.util.Log;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -32,7 +33,7 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.Flags;
-import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.lang.ref.WeakReference;
@@ -42,6 +43,8 @@
 @WorkerThread
 public class InstallSessionTracker extends PackageInstaller.SessionCallback {
 
+    public static final String TAG = "InstallSessionTracker";
+
     // Lazily initialized
     private SparseArray<PackageUserKey> mActiveSessions = null;
 
@@ -76,6 +79,11 @@
         }
         SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback);
         if (sessionInfo != null) {
+            FileLog.d(TAG, "onCreated: Install session created for"
+                    + " appPackageName=" + sessionInfo.getAppPackageName()
+                    + ", sessionId=" + sessionInfo.getSessionId()
+                    + ", appIcon=" + sessionInfo.getAppIcon()
+                    + ", appLabel=" + sessionInfo.getAppLabel());
             callback.onInstallSessionCreated(PackageInstallInfo.fromInstallingState(sessionInfo));
         }
 
@@ -103,6 +111,10 @@
         activeSessions.remove(sessionId);
 
         if (key != null && key.mPackageName != null) {
+            FileLog.d(TAG, "onFinished: active install session finished for"
+                    + " appPackageName=" + key.mPackageName
+                    + ", sessionId=" + sessionId
+                    + ", success=" + success);
             String packageName = key.mPackageName;
             PackageInstallInfo info = PackageInstallInfo.fromState(
                     success ? STATUS_INSTALLED : STATUS_FAILED,
@@ -142,6 +154,11 @@
         }
         SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback);
         if (sessionInfo != null) {
+            Log.d(TAG, "onBadgingChanged: badging info changed for"
+                    + " appPackageName=" + sessionInfo.getAppPackageName()
+                    + ", sessionId=" + sessionInfo.getSessionId()
+                    + ", appIcon=" + sessionInfo.getAppIcon()
+                    + ", appLabel=" + sessionInfo.getAppLabel());
             helper.tryQueuePromiseAppIcon(sessionInfo);
         }
     }
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
index 667136a..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 {
@@ -77,8 +78,10 @@
             WorkspaceItemInfo info = new WorkspaceItemInfo(si, context);
             // Apply the unbadged icon synchronously using the caching logic directly and
             // fetch the actual icon asynchronously.
-            info.bitmap = new ShortcutCachingLogic().loadIcon(context, si);
-            LauncherAppState.getInstance(context).getModel().updateAndBindWorkspaceItem(info, si);
+            LauncherAppState app = LauncherAppState.getInstance(context);
+            info.bitmap = CacheableShortcutCachingLogic.INSTANCE.loadIcon(
+                    context, app.getIconCache(), new CacheableShortcutInfo(si, context));
+            app.getModel().updateAndBindWorkspaceItem(info, si);
             return info;
         } else {
             return null;
diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index 351ebce..409174e 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -26,9 +26,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Process;
@@ -40,9 +40,10 @@
 
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.icons.ComponentWithLabelAndIcon;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.BaseIconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
@@ -52,16 +53,26 @@
 /**
  * Wrapper class for representing a shortcut configure activity.
  */
-public abstract class ShortcutConfigActivityInfo implements ComponentWithLabelAndIcon {
+public abstract class ShortcutConfigActivityInfo implements CachedObject {
 
     private static final String TAG = "SCActivityInfo";
 
     private final ComponentName mCn;
     private final UserHandle mUser;
+    private final ApplicationInfoWrapper mInfoWrapper;
 
-    protected ShortcutConfigActivityInfo(ComponentName cn, UserHandle user) {
+    protected ShortcutConfigActivityInfo(
+            ComponentName cn, UserHandle user, ApplicationInfoWrapper infoWrapper) {
         mCn = cn;
         mUser = user;
+        mInfoWrapper = infoWrapper;
+    }
+
+    protected ShortcutConfigActivityInfo(
+            ComponentName cn, UserHandle user, Context context) {
+        mCn = cn;
+        mUser = user;
+        mInfoWrapper = new ApplicationInfoWrapper(context, cn.getPackageName(), user);
     }
 
     @Override
@@ -79,7 +90,7 @@
     }
 
     @Override
-    public abstract Drawable getFullResIcon(IconCache cache);
+    public abstract Drawable getFullResIcon(BaseIconCache cache);
 
     /**
      * Return a WorkspaceItemInfo, if it can be created directly on drop, without requiring any
@@ -89,6 +100,12 @@
         return null;
     }
 
+    @Nullable
+    @Override
+    public ApplicationInfo getApplicationInfo() {
+        return mInfoWrapper.getInfo();
+    }
+
     public boolean startConfigActivity(Activity activity, int requestCode) {
         Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT)
                 .setComponent(getComponent());
@@ -107,7 +124,7 @@
     }
 
     /**
-     * Returns true if various properties ({@link #getLabel(PackageManager)},
+     * Returns true if various properties ({@link #getLabel()},
      * {@link #getFullResIcon}) can be safely persisted.
      */
     public boolean isPersistable() {
@@ -120,18 +137,19 @@
         private final LauncherActivityInfo mInfo;
 
         public ShortcutConfigActivityInfoVO(LauncherActivityInfo info) {
-            super(info.getComponentName(), info.getUser());
+            super(info.getComponentName(), info.getUser(),
+                    new ApplicationInfoWrapper(info.getApplicationInfo()));
             mInfo = info;
         }
 
         @Override
-        public CharSequence getLabel(PackageManager pm) {
+        public CharSequence getLabel() {
             return mInfo.getLabel();
         }
 
         @Override
-        public Drawable getFullResIcon(IconCache cache) {
-            return cache.getFullResIcon(mInfo);
+        public Drawable getFullResIcon(BaseIconCache cache) {
+            return cache.getFullResIcon(mInfo.getActivityInfo());
         }
 
         @Override
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 7339111..e861961 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -183,6 +183,11 @@
         mUserToSerialMap.put(userHandle, info);
     }
 
+    @VisibleForTesting
+    public void putToPreInstallCache(UserHandle userHandle, List<String> preInstalledApps) {
+        mUserToPreInstallAppMap.put(userHandle, preInstalledApps);
+    }
+
     /**
      * @see UserManager#getUserProfiles()
      */
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 4d4a8f7..c2debfa 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.popup;
 
-import static androidx.core.content.ContextCompat.getColorStateList;
-
 import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.app.animation.Interpolators.LINEAR;
@@ -56,8 +54,6 @@
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
 
-import java.util.Arrays;
-
 /**
  * A container for shortcuts to deep links and notifications associated with an app.
  *
@@ -130,7 +126,7 @@
     // Tag for Views that have children that will need to be iterated to add styling.
     private final String mIterateChildrenTag;
 
-    protected final int[] mColorIds;
+    protected final int[] mColors;
 
     public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
@@ -142,8 +138,7 @@
 
         // Initialize arrow view
         final Resources resources = getResources();
-        mArrowColor = getColorStateList(getContext(), R.color.popup_color_background)
-                .getDefaultColor();
+        mArrowColor = Themes.getAttrColor(getContext(), R.attr.materialColorSurfaceContainer);
         mChildContainerMargin = resources.getDimensionPixelSize(R.dimen.popup_margin);
         mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
         mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
@@ -158,21 +153,25 @@
         mRoundedTop = new GradientDrawable();
         int popupPrimaryColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
         mRoundedTop.setColor(popupPrimaryColor);
-        mRoundedTop.setCornerRadii(new float[] { mOutlineRadius, mOutlineRadius, mOutlineRadius,
+        mRoundedTop.setCornerRadii(new float[]{mOutlineRadius, mOutlineRadius, mOutlineRadius,
                 mOutlineRadius, smallerRadius, smallerRadius, smallerRadius, smallerRadius});
 
         mRoundedBottom = new GradientDrawable();
         mRoundedBottom.setColor(popupPrimaryColor);
-        mRoundedBottom.setCornerRadii(new float[] { smallerRadius, smallerRadius, smallerRadius,
+        mRoundedBottom.setCornerRadii(new float[]{smallerRadius, smallerRadius, smallerRadius,
                 smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius});
 
         mIterateChildrenTag = getContext().getString(R.string.popup_container_iterate_children);
 
         if (mActivityContext.canUseMultipleShadesForPopup()) {
-            mColorIds = new int[]{R.color.popup_shade_first, R.color.popup_shade_second,
-                    R.color.popup_shade_third};
+            mColors = new int[]{
+                    getContext().getColor(R.color.popup_shade_first),
+                    getContext().getColor(R.color.popup_shade_second),
+                    getContext().getColor(R.color.popup_shade_third)
+            };
         } else {
-            mColorIds = new int[]{R.color.popup_color_background};
+            mColors = new int[]{Themes.getAttrColor(getContext(),
+                    R.attr.materialColorSurfaceContainer)};
         }
     }
 
@@ -219,15 +218,14 @@
     }
 
     /**
-     * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColorIds}.
+     * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColors}.
      *                        Otherwise, we will use this color for all child views.
      */
     protected void assignMarginsAndBackgrounds(ViewGroup viewGroup, int backgroundColor) {
         int[] colors = null;
         if (backgroundColor == Color.TRANSPARENT) {
             // Lazily get the colors so they match the current wallpaper colors.
-            colors = Arrays.stream(mColorIds).map(
-                    r -> getColorStateList(getContext(), r).getDefaultColor()).toArray();
+            colors = mColors;
         }
 
         int count = viewGroup.getChildCount();
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index fb463f7..8a5e388 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -24,28 +24,20 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.dot.DotInfo;
-import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.picker.WidgetRecommendationCategory;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 
 /**
  * Provides data for the popup menu that appears after long-clicking on apps.
@@ -62,13 +54,6 @@
     /** Maps packages to their DotInfo's . */
     private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
 
-    /** All installed widgets. */
-    private List<WidgetsListBaseEntry> mAllWidgets = List.of();
-    /** Widgets that can be recommended to the users. */
-    private List<ItemInfo> mRecommendedWidgets = List.of();
-
-    private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
-
     public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
         mNotificationDotsChangeListener = notificationDotsChangeListener;
     }
@@ -183,102 +168,8 @@
         })) ? dotInfo : null;
     }
 
-    /**
-     * Sets a list of recommended widgets ordered by their order of appearance in the widgets
-     * recommendation UI.
-     */
-    public void setRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
-        mRecommendedWidgets = recommendedWidgets;
-        mChangeListener.onRecommendedWidgetsBound();
-    }
-
-    public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
-        mAllWidgets = allWidgets;
-        mChangeListener.onWidgetsBound();
-    }
-
-    public void setChangeListener(PopupDataChangeListener listener) {
-        mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
-    }
-
-    public List<WidgetsListBaseEntry> getAllWidgets() {
-        return mAllWidgets;
-    }
-
-    /** Returns a list of recommended widgets. */
-    public List<WidgetItem> getRecommendedWidgets() {
-        HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
-        mAllWidgets.stream()
-                .filter(entry -> entry instanceof WidgetsListContentEntry)
-                .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets
-                        .forEach(widget -> allWidgetItems.put(
-                                new ComponentKey(widget.componentName, widget.user), widget)));
-        return mRecommendedWidgets.stream()
-                .map(recommendedWidget -> allWidgetItems.get(
-                        new ComponentKey(recommendedWidget.getTargetComponent(),
-                                recommendedWidget.user)))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /** Returns the recommended widgets mapped by their category. */
-    @NonNull
-    public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
-        Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
-                .filter(entry -> entry instanceof WidgetsListContentEntry)
-                .flatMap(entry -> entry.mWidgets.stream())
-                .distinct()
-                .collect(Collectors.toMap(
-                        widget -> new ComponentKey(widget.componentName, widget.user),
-                        Function.identity()
-                ));
-        return mRecommendedWidgets.stream()
-                .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo
-                        && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null)
-                .collect(Collectors.groupingBy(
-                        it -> ((PendingAddWidgetInfo) it).recommendationCategory,
-                        Collectors.collectingAndThen(
-                                Collectors.toList(),
-                                list -> list.stream()
-                                        .map(it -> allWidgetItems.get(
-                                                new ComponentKey(it.getTargetComponent(),
-                                                        it.user)))
-                                        .filter(Objects::nonNull)
-                                        .collect(Collectors.toList())
-                        )
-                ));
-    }
-
-    public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
-        return mAllWidgets.stream()
-                .filter(row -> row instanceof WidgetsListContentEntry
-                        && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
-                .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
-                .filter(widget -> packageUserKey.mUser.equals(widget.user))
-                .collect(Collectors.toList());
-    }
-
-    /** Gets the WidgetsListContentEntry for the currently selected header. */
-    public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey) {
-        return (WidgetsListContentEntry) mAllWidgets.stream()
-                .filter(row -> row instanceof WidgetsListContentEntry
-                        && PackageUserKey.fromPackageItemInfo(row.mPkgItem).equals(packageUserKey))
-                .findAny()
-                .orElse(null);
-    }
-
     public void dump(String prefix, PrintWriter writer) {
         writer.println(prefix + "PopupDataProvider:");
         writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
     }
-
-    public interface PopupDataChangeListener {
-
-        PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
-
-        default void onWidgetsBound() { }
-
-        /** A callback to get notified when recommended widgets are bound. */
-        default void onRecommendedWidgetsBound() { }
-    }
 }
diff --git a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
index 4c94f94..1fd3557 100644
--- a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
+++ b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
@@ -19,6 +19,8 @@
 import android.view.View;
 
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener;
 
 /**
  * Utility class to handle updates while the popup is visible (like widgets and
@@ -27,7 +29,7 @@
  * @param <T> The activity on which the popup shows
  */
 public abstract class PopupLiveUpdateHandler<T extends Context & ActivityContext> implements
-        PopupDataProvider.PopupDataChangeListener, View.OnAttachStateChangeListener {
+        WidgetPickerDataChangeListener, View.OnAttachStateChangeListener {
 
     protected final T mContext;
     protected final PopupContainerWithArrow<T> mPopupContainerWithArrow;
@@ -40,19 +42,25 @@
 
     @Override
     public void onViewAttachedToWindow(View view) {
-        PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+        WidgetPickerDataProvider widgetsDataProvider = mContext.getWidgetPickerDataProvider();
 
-        if (popupDataProvider != null) {
-            popupDataProvider.setChangeListener(this);
+        if (widgetsDataProvider != null) {
+            widgetsDataProvider.setChangeListener(this);
         }
     }
 
     @Override
     public void onViewDetachedFromWindow(View view) {
-        PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+        WidgetPickerDataProvider widgetsDataProvider = mContext.getWidgetPickerDataProvider();
 
-        if (popupDataProvider != null) {
-            popupDataProvider.setChangeListener(null);
+        if (widgetsDataProvider != null) {
+            widgetsDataProvider.setChangeListener(null);
         }
     }
+
+    @Override
+    public void onWidgetsBound() {} // NO_OP
+
+    @Override
+    public void onRecommendedWidgetsBound() {} // NO_OP
 }
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/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 83e9810..63c9d94 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,18 +1,21 @@
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser;
 
-import android.app.ActivityOptions;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Process;
 import android.os.UserHandle;
+import android.util.Log;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
@@ -24,24 +27,26 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.AbstractFloatingViewHelper;
 import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.SecondaryDropTarget;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.PrivateProfileManager;
-import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.Snackbar;
 import com.android.launcher3.widget.WidgetsBottomSheet;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
 
 import java.util.Arrays;
-import java.util.List;
 
 /**
  * Represents a system shortcut for a given app. The shortcut should have a label and icon, and an
@@ -53,6 +58,7 @@
  */
 public abstract class SystemShortcut<T extends ActivityContext> extends ItemInfo
         implements View.OnClickListener {
+    private static final String TAG = "SystemShortcut";
 
     private final int mIconResId;
     protected final int mLabelResId;
@@ -107,11 +113,12 @@
     }
 
     public static final Factory<ActivityContext> WIDGETS = (context, itemInfo, originalView) -> {
-        if (itemInfo.getTargetComponent() == null) return null;
-        final List<WidgetItem> widgets =
-                context.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
-                        itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
-        if (widgets.isEmpty()) {
+        final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(itemInfo);
+        if (packageUserKey == null) return null;
+
+        final WidgetPickerData data = context.getWidgetPickerDataProvider().get();
+        if (findAllWidgetsForPackageUser(data, packageUserKey).isEmpty()) {
+            // hides widget picker shortcut if there are no widgets for the package.
             return null;
         }
         return new Widgets(context, itemInfo, originalView);
@@ -179,10 +186,12 @@
 
         @Override
         public void onClick(View view) {
-            dismissTaskMenuView();
             Rect sourceBounds = Utilities.getViewBounds(view);
+            ActivityOptionsWrapper options = mTarget.getActivityLaunchOptions(view, mItemInfo);
+            // Dismiss the taskMenu when the app launch animation is complete
+            options.onEndCallback.add(this::dismissTaskMenuView);
             PackageManagerHelper.startDetailsActivityForInfo(view.getContext(), mItemInfo,
-                    sourceBounds, ActivityOptions.makeBasic().toBundle());
+                    sourceBounds, options.toBundle());
             mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
                     .log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP);
         }
@@ -329,6 +338,14 @@
             mTarget.getStatsLogManager().logger()
                     .withItemInfo(mItemInfo)
                     .log(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP);
+            if (Flags.enableDismissPredictionUndo()) {
+                Snackbar.show(mTarget,
+                        view.getContext().getString(R.string.item_removed), R.string.undo,
+                        () -> { }, () ->
+                            mTarget.getStatsLogManager().logger()
+                                    .withItemInfo(mItemInfo)
+                                    .log(LAUNCHER_DISMISS_PREDICTION_UNDO));
+            }
         }
     }
 
@@ -382,4 +399,63 @@
         mAbstractFloatingViewHelper.closeOpenViews(mTarget, true,
                 AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
     }
+
+    public static final Factory<ActivityContext> BUBBLE_SHORTCUT =
+            (activity, itemInfo, originalView) -> {
+                if ((itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                        && (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION)
+                        && !(itemInfo instanceof WorkspaceItemInfo)) {
+                    return null;
+                }
+                return new BubbleShortcut(activity, itemInfo, originalView);
+            };
+
+    public interface BubbleActivityStarter {
+        /** Tell SysUI to show the provided shortcut in a bubble. */
+        void showShortcutBubble(ShortcutInfo info);
+
+        /** Tell SysUI to show the provided intent in a bubble. */
+        void showAppBubble(Intent intent);
+    }
+
+    public static class BubbleShortcut<T extends ActivityContext> extends SystemShortcut<T> {
+
+        private BubbleActivityStarter mStarter;
+
+        public BubbleShortcut(T target, ItemInfo itemInfo, View originalView) {
+            super(R.drawable.ic_bubble_button, R.string.bubble, target,
+                    itemInfo, originalView);
+            if (target instanceof BubbleActivityStarter) {
+                mStarter = (BubbleActivityStarter) target;
+            }
+        }
+
+        @Override
+        public void onClick(View view) {
+            dismissTaskMenuView();
+            if (mStarter == null) {
+                Log.w(TAG, "starter null!");
+                return;
+            }
+            // TODO: handle GroupTask (single) items so that recent items in taskbar work
+            if (mItemInfo instanceof WorkspaceItemInfo) {
+                WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo;
+                ShortcutInfo shortcutInfo = workspaceItemInfo.getDeepShortcutInfo();
+                if (shortcutInfo != null) {
+                    mStarter.showShortcutBubble(shortcutInfo);
+                    return;
+                }
+            }
+            // If we're here check for an intent
+            Intent intent = mItemInfo.getIntent();
+            if (intent != null) {
+                if (intent.getPackage() == null) {
+                    intent.setPackage(mItemInfo.getTargetPackage());
+                }
+                mStarter.showAppBubble(intent);
+            } else {
+                Log.w(TAG, "unable to bubble, no intent: " + mItemInfo);
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
deleted file mode 100644
index 3ae643e..0000000
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.provider;
-
-import static com.android.launcher3.LauncherSettings.Favorites.getColumns;
-import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ShortcutInfo;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Icon;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserManager;
-import android.text.TextUtils;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.model.LoaderCursor;
-import com.android.launcher3.model.UserManagerState;
-import com.android.launcher3.pm.PinRequestHelper;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.PackageManagerHelper;
-
-/**
- * A set of utility methods for Launcher DB used for DB updates and migration.
- */
-public class LauncherDbUtils {
-    /**
-     * Returns a string which can be used as a where clause for DB query to match the given itemId
-     */
-    public static String itemIdMatch(int itemId) {
-        return "_id=" + itemId;
-    }
-
-    public static IntArray queryIntArray(boolean distinct, SQLiteDatabase db, String tableName,
-            String columnName, String selection, String groupBy, String orderBy) {
-        IntArray out = new IntArray();
-        try (Cursor c = db.query(distinct, tableName, new String[] { columnName }, selection, null,
-                groupBy, null, orderBy, null)) {
-            while (c.moveToNext()) {
-                out.add(c.getInt(0));
-            }
-        }
-        return out;
-    }
-
-    public static boolean tableExists(SQLiteDatabase db, String tableName) {
-        try (Cursor c = db.query(true, "sqlite_master", new String[] {"tbl_name"},
-                "tbl_name = ?", new String[] {tableName},
-                null, null, null, null, null)) {
-            return c.getCount() > 0;
-        }
-    }
-
-    public static void dropTable(SQLiteDatabase db, String tableName) {
-        db.execSQL("DROP TABLE IF EXISTS " + tableName);
-    }
-
-    /** Copy fromTable in fromDb to toTable in toDb. */
-    public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
-            String toTable, Context context) {
-        long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
-                Process.myUserHandle());
-        dropTable(toDb, toTable);
-        Favorites.addTableToDb(toDb, userSerial, false, toTable);
-        if (fromDb != toDb) {
-            toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
-            toDb.execSQL(
-                    "INSERT INTO " + toTable + " SELECT " + getColumns(userSerial)
-                        + " FROM from_db." + fromTable);
-            toDb.execSQL("DETACH DATABASE 'from_db'");
-        } else {
-            toDb.execSQL("INSERT INTO " + toTable + " SELECT " + getColumns(userSerial) + " FROM "
-                    + fromTable);
-        }
-    }
-
-    /**
-     * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher.
-     * Removes any invalid shortcut or any shortcut which requires some permission to launch
-     */
-    public static void migrateLegacyShortcuts(Context context, SQLiteDatabase db) {
-        Cursor c = db.query(
-                Favorites.TABLE_NAME, null, "itemType = 1", null, null, null, null);
-        UserManagerState ums = new UserManagerState();
-        PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
-        ums.init(UserCache.INSTANCE.get(context),
-                context.getSystemService(UserManager.class));
-        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper,
-                null);
-        IntSet deletedShortcuts = new IntSet();
-
-        while (lc.moveToNext()) {
-            if (lc.user != Process.myUserHandle()) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            Intent intent = lc.parseIntent();
-            if (intent == null) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            if (TextUtils.isEmpty(lc.getTitle())) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-
-            // Make sure the target intent can be launched without any permissions. Otherwise remove
-            // the shortcut
-            ResolveInfo ri = context.getPackageManager().resolveActivity(intent, 0);
-            if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            PersistableBundle extras = new PersistableBundle();
-            extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, ri.activityInfo.packageName);
-            ShortcutInfo.Builder infoBuilder = new ShortcutInfo.Builder(
-                    context, "migrated_shortcut-" + lc.id)
-                    .setIntent(intent)
-                    .setExtras(extras)
-                    .setShortLabel(lc.getTitle());
-
-            Bitmap bitmap = null;
-            byte[] iconData = lc.getIconBlob();
-            if (iconData != null) {
-                bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
-            }
-            if (bitmap != null) {
-                infoBuilder.setIcon(Icon.createWithBitmap(bitmap));
-            }
-
-            ShortcutInfo info = infoBuilder.build();
-            try {
-                if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
-                    deletedShortcuts.add(lc.id);
-                    continue;
-                }
-            } catch (Exception e) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            ContentValues update = new ContentValues();
-            update.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_DEEP_SHORTCUT);
-            update.put(Favorites.INTENT,
-                    ShortcutKey.makeIntent(info.getId(), context.getPackageName()).toUri(0));
-            db.update(Favorites.TABLE_NAME, update, "_id = ?",
-                    new String[] {Integer.toString(lc.id)});
-        }
-        lc.close();
-        if (!deletedShortcuts.isEmpty()) {
-            db.delete(Favorites.TABLE_NAME,
-                    Utilities.createDbSelectionQuery(Favorites._ID, deletedShortcuts.getArray()),
-                    null);
-        }
-
-        // Drop the unused columns
-        db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconPackage;");
-        db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconResource;");
-    }
-
-    /**
-     * Utility class to simplify managing sqlite transactions
-     */
-    public static class SQLiteTransaction implements AutoCloseable {
-        private final SQLiteDatabase mDb;
-
-        public SQLiteTransaction(SQLiteDatabase db) {
-            mDb = db;
-            db.beginTransaction();
-        }
-
-        public void commit() {
-            mDb.setTransactionSuccessful();
-        }
-
-        @Override
-        public void close() {
-            mDb.endTransaction();
-        }
-
-        public SQLiteDatabase getDb() {
-            return mDb;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt
new file mode 100644
index 0000000..3c68e46
--- /dev/null
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.provider
+
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Icon
+import android.os.PersistableBundle
+import android.os.Process
+import android.os.UserManager
+import android.text.TextUtils
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.LoaderCursor
+import com.android.launcher3.model.UserManagerState
+import com.android.launcher3.pm.PinRequestHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.PackageManagerHelper
+
+/** A set of utility methods for Launcher DB used for DB updates and migration. */
+object LauncherDbUtils {
+    /**
+     * Returns a string which can be used as a where clause for DB query to match the given itemId
+     */
+    @JvmStatic fun itemIdMatch(itemId: Int): String = "_id=$itemId"
+
+    @JvmStatic
+    fun queryIntArray(
+        distinct: Boolean,
+        db: SQLiteDatabase,
+        tableName: String,
+        columnName: String,
+        selection: String?,
+        groupBy: String?,
+        orderBy: String?,
+    ): IntArray {
+        val out = IntArray()
+        db.query(
+                distinct,
+                tableName,
+                arrayOf(columnName),
+                selection,
+                null,
+                groupBy,
+                null,
+                orderBy,
+                null,
+            )
+            .use { c ->
+                while (c.moveToNext()) {
+                    out.add(c.getInt(0))
+                }
+            }
+        return out
+    }
+
+    @JvmStatic
+    fun tableExists(db: SQLiteDatabase, tableName: String): Boolean =
+        db.query(
+                /* distinct = */ true,
+                /* table = */ "sqlite_master",
+                /* columns = */ arrayOf("tbl_name"),
+                /* selection = */ "tbl_name = ?",
+                /* selectionArgs = */ arrayOf(tableName),
+                /* groupBy = */ null,
+                /* having = */ null,
+                /* orderBy = */ null,
+                /* limit = */ null,
+                /* cancellationSignal = */ null,
+            )
+            .use { c ->
+                return c.count > 0
+            }
+
+    @JvmStatic
+    fun dropTable(db: SQLiteDatabase, tableName: String) =
+        db.execSQL("DROP TABLE IF EXISTS $tableName")
+
+    /** Copy fromTable in fromDb to toTable in toDb. */
+    @JvmStatic
+    fun copyTable(
+        fromDb: SQLiteDatabase,
+        fromTable: String,
+        toDb: SQLiteDatabase,
+        toTable: String,
+        context: Context,
+    ) {
+        val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
+        dropTable(toDb, toTable)
+        LauncherSettings.Favorites.addTableToDb(toDb, userSerial, false, toTable)
+        if (fromDb != toDb) {
+            toDb.run {
+                execSQL("ATTACH DATABASE '${fromDb.path}' AS from_db")
+                execSQL(
+                    "INSERT INTO $toTable SELECT ${LauncherSettings.Favorites.getColumns(userSerial)} FROM from_db.$fromTable"
+                )
+                execSQL("DETACH DATABASE 'from_db'")
+            }
+        } else {
+            toDb.run {
+                execSQL(
+                    "INSERT INTO $toTable SELECT ${
+                        LauncherSettings.Favorites.getColumns(
+                            userSerial
+                        )
+                    } FROM $fromTable"
+                )
+            }
+        }
+    }
+
+    /**
+     * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
+     * shortcut or any shortcut which requires some permission to launch
+     */
+    @JvmStatic
+    fun migrateLegacyShortcuts(context: Context, db: SQLiteDatabase) {
+        val c =
+            db.query(
+                LauncherSettings.Favorites.TABLE_NAME,
+                null,
+                "itemType = 1",
+                null,
+                null,
+                null,
+                null,
+            )
+        val pmHelper = PackageManagerHelper.INSTANCE[context]
+        val ums = UserManagerState()
+        ums.run {
+            init(UserCache.INSTANCE[context], context.getSystemService(UserManager::class.java))
+        }
+        val lc = LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper, null)
+        val deletedShortcuts = IntSet()
+
+        while (lc.moveToNext()) {
+            if (lc.user !== Process.myUserHandle()) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val intent = lc.parseIntent()
+            if (intent == null) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            if (TextUtils.isEmpty(lc.title)) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+
+            // Make sure the target intent can be launched without any permissions. Otherwise remove
+            // the shortcut
+            val ri = context.packageManager.resolveActivity(intent, 0)
+            if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val extras =
+                PersistableBundle().apply {
+                    putString(
+                        IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE,
+                        ri.activityInfo.packageName,
+                    )
+                }
+            val infoBuilder =
+                ShortcutInfo.Builder(context, "migrated_shortcut-${lc.id}")
+                    .setIntent(intent)
+                    .setExtras(extras)
+                    .setShortLabel(lc.title)
+
+            var bitmap: Bitmap? = null
+            val iconData = lc.iconBlob
+            if (iconData != null) {
+                bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.size)
+            }
+            if (bitmap != null) {
+                infoBuilder.setIcon(Icon.createWithBitmap(bitmap))
+            }
+
+            val info = infoBuilder.build()
+            try {
+                if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
+                    deletedShortcuts.add(lc.id)
+                    continue
+                }
+            } catch (e: Exception) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val update =
+                ContentValues().apply {
+                    put(
+                        LauncherSettings.Favorites.ITEM_TYPE,
+                        LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+                    )
+                    put(
+                        LauncherSettings.Favorites.INTENT,
+                        ShortcutKey.makeIntent(info.id, context.packageName).toUri(0),
+                    )
+                }
+            db.update(
+                LauncherSettings.Favorites.TABLE_NAME,
+                update,
+                "_id = ?",
+                arrayOf(lc.id.toString()),
+            )
+        }
+        lc.close()
+        if (deletedShortcuts.isEmpty.not()) {
+            db.delete(
+                /* table = */ LauncherSettings.Favorites.TABLE_NAME,
+                /* whereClause = */ Utilities.createDbSelectionQuery(
+                    LauncherSettings.Favorites._ID,
+                    deletedShortcuts.array,
+                ),
+                /* whereArgs = */ null,
+            )
+        }
+
+        // Drop the unused columns
+        db.run {
+            execSQL("ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconPackage;")
+            execSQL(
+                "ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconResource;"
+            )
+        }
+    }
+
+    /** Utility class to simplify managing sqlite transactions */
+    class SQLiteTransaction(val db: SQLiteDatabase) : AutoCloseable {
+        init {
+            db.beginTransaction()
+        }
+
+        fun commit() = db.setTransactionSuccessful()
+
+        override fun close() = db.endTransaction()
+    }
+}
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 21897bf..8db981f 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -51,7 +51,6 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherFiles;
@@ -75,7 +74,6 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LogConfig;
 
-import java.io.File;
 import java.io.InvalidObjectException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -125,17 +123,14 @@
         // executed again.
         LauncherPrefs.get(context).removeSync(RESTORE_DEVICE);
 
-        if (Flags.enableNarrowGridRestore()) {
-            String oldPhoneFileName = idp.dbFile;
-            List<String> previousDbs = existingDbs();
-            removeOldDBs(context, oldPhoneFileName);
-            // The idp before this contains data about the old phone, after this it becomes the idp
-            // of the current phone.
-            idp.reset(context);
-            trySettingPreviousGidAsCurrent(context, idp, oldPhoneFileName, previousDbs);
-        } else {
-            idp.reinitializeAfterRestore(context);
-        }
+        DeviceGridState deviceGridState = new DeviceGridState(context);
+        String oldPhoneFileName = deviceGridState.getDbFile();
+        List<String> previousDbs = existingDbs(context);
+        removeOldDBs(context, oldPhoneFileName);
+        // The idp before this contains data about the old phone, after this it becomes the idp
+        // of the current phone.
+        idp.reset(context);
+        trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
     }
 
 
@@ -143,12 +138,13 @@
      * Try setting the gird used in the previous phone to the new one. If the current device doesn't
      * support the previous grid option it will not be set.
      */
-    private static void trySettingPreviousGidAsCurrent(Context context, InvariantDeviceProfile idp,
+    private static void trySettingPreviousGridAsCurrent(Context context, InvariantDeviceProfile idp,
             String oldPhoneDbFileName, List<String> previousDbs) {
         InvariantDeviceProfile.GridOption oldPhoneGridOption = idp.getGridOptionFromFileName(
                 context, oldPhoneDbFileName);
         // The grid option could be null if current phone doesn't support the previous db.
         if (oldPhoneGridOption != null) {
+
             /* If the user only used the default db on the previous phone and the new default db is
              * bigger than or equal to the previous one, then keep the new default db */
             if (previousDbs.size() == 1 && oldPhoneGridOption.numColumns <= idp.numColumns
@@ -166,17 +162,19 @@
     /**
      * Returns a list of paths of the existing launcher dbs.
      */
-    private static List<String> existingDbs() {
+    @VisibleForTesting
+    public static List<String> existingDbs(Context context) {
         // At this point idp.dbFile contains the name of the dbFile from the previous phone
         return LauncherFiles.GRID_DB_FILES.stream()
-                .filter(dbName -> new File(dbName).exists())
+                .filter(dbName -> context.getDatabasePath(dbName).exists())
                 .toList();
     }
 
     /**
      * Only keep the last database used on the previous device.
      */
-    private static void removeOldDBs(Context context, String oldPhoneDbFileName) {
+    @VisibleForTesting
+    public static void removeOldDBs(Context context, String oldPhoneDbFileName) {
         // At this point idp.dbFile contains the name of the dbFile from the previous phone
         LauncherFiles.GRID_DB_FILES.stream()
                 .filter(dbName -> !dbName.equals(oldPhoneDbFileName))
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 6d6b3b6..82229f8 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -17,12 +17,13 @@
 package com.android.launcher3.recyclerview
 
 import android.content.Context
+import android.util.Log
 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
@@ -43,6 +44,12 @@
     var hasWorkProfile = false
     private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
 
+    companion object {
+        private const val TAG = "AllAppsRecyclerViewPool"
+        private const val NULL_LAYOUT_MANAGER_ERROR_STRING =
+            "activeRv's layoutManager should not be null"
+    }
+
     /**
      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
      */
@@ -54,6 +61,15 @@
             return
         }
 
+        if (activeRv.layoutManager == null) {
+            if (BuildConfig.IS_STUDIO_BUILD) {
+                throw IllegalStateException(NULL_LAYOUT_MANAGER_ERROR_STRING)
+            } else {
+                Log.e(TAG, NULL_LAYOUT_MANAGER_ERROR_STRING)
+            }
+            return
+        }
+
         // Create a separate context dedicated for all apps preinflation thread. The goal is to
         // create a separate AssetManager obj internally to avoid lock contention with
         // AssetManager obj that is associated with the launcher context on the main thread.
@@ -61,7 +77,7 @@
             ActivityContextDelegate(
                 context.createConfigurationContext(context.resources.configuration),
                 Themes.getActivityThemeRes(context),
-                context
+                context,
             )
 
         // Because we perform onCreateViewHolder() on worker thread, we need a separate
@@ -74,9 +90,10 @@
                     context,
                     context.appsView.layoutInflater.cloneInContext(allAppsPreInflationContext),
                     null,
-                    null
+                    null,
                 ) {
                 override fun setAppsPerRow(appsPerRow: Int) = Unit
+
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
@@ -90,6 +107,11 @@
                         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) {
+                            break
+                        }
                         list.add(
                             adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
                         )
@@ -101,7 +123,7 @@
                     for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
                         putRecycledView(viewHolders[i])
                     }
-                }
+                },
             )
         mCancellableTask = task
         VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
@@ -121,18 +143,15 @@
      * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
      * suffice fast scrolling.
      *
-     * 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.
+     * 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.
      */
     fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
         var targetPreinflateCount =
             PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
                 EXTRA_ICONS_COUNT
-        if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) {
-            val grid = ActivityContext.lookupContext<T>(context).deviceProfile
-            targetPreinflateCount += grid.maxAllAppsRowCount * grid.numShownAllAppsColumns
-        }
+        val grid = ActivityContext.lookupContext<T>(context).deviceProfile
+        targetPreinflateCount += grid.maxAllAppsRowCount * grid.numShownAllAppsColumns
         if (hasWorkProfile) {
             targetPreinflateCount *= 2
         }
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 0299a23..9b3292d 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -59,6 +59,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -76,6 +77,7 @@
     private View mAppsButton;
 
     private PopupDataProvider mPopupDataProvider;
+    private WidgetPickerDataProvider mWidgetPickerDataProvider;
 
     private boolean mAppDrawerShown = false;
 
@@ -315,6 +317,11 @@
     }
 
     @Override
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
+    }
+
+    @Override
     public OnClickListener getItemOnClickListener() {
         return this::onIconClicked;
     }
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 52ce4e8..5851f62 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -26,6 +26,7 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.Settings;
@@ -44,11 +45,13 @@
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback;
 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartScreenCallback;
+import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceGroup.PreferencePositionCallback;
 import androidx.preference.PreferenceScreen;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BuildConfig;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherFiles;
 import com.android.launcher3.R;
 import com.android.launcher3.states.RotationHelper;
@@ -64,6 +67,8 @@
     @VisibleForTesting
     static final String DEVELOPER_OPTIONS_KEY = "pref_developer_options";
 
+    public static final String FIXED_LANDSCAPE_MODE = "pref_fixed_landscape_mode";
+
     private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging";
 
     public static final String EXTRA_FRAGMENT_ARGS = ":settings:fragment_args";
@@ -165,6 +170,7 @@
         private boolean mRestartOnResume = false;
 
         private String mHighLightKey;
+
         private boolean mPreferenceHighlighted = false;
 
         @Override
@@ -198,11 +204,62 @@
                 }
             }
 
+            // If the target preference is not in the current preference screen, find the parent
+            // preference screen that contains the target preference and set it as the preference
+            // screen.
+            if (Flags.navigateToChildPreference()
+                    && mHighLightKey != null
+                    && !isKeyInPreferenceGroup(mHighLightKey, screen)) {
+                final PreferenceScreen parentPreferenceScreen =
+                        findParentPreference(screen, mHighLightKey);
+                if (parentPreferenceScreen != null && getActivity() != null) {
+                    if (!TextUtils.isEmpty(parentPreferenceScreen.getTitle())) {
+                        getActivity().setTitle(parentPreferenceScreen.getTitle());
+                    }
+                    setPreferenceScreen(parentPreferenceScreen);
+                    return;
+                }
+            }
+
             if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) {
                 getActivity().setTitle(getPreferenceScreen().getTitle());
             }
         }
 
+        private boolean isKeyInPreferenceGroup(String targetKey, PreferenceGroup parent) {
+            for (int i = 0; i < parent.getPreferenceCount(); i++) {
+                Preference pref = parent.getPreference(i);
+                if (pref.getKey() != null && pref.getKey().equals(targetKey)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Finds the parent preference screen for the given target key.
+         *
+         * @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
+         */
+        @Nullable
+        private PreferenceScreen findParentPreference(PreferenceScreen parent, String targetKey) {
+            for (int i = 0; i < parent.getPreferenceCount(); i++) {
+                Preference pref = parent.getPreference(i);
+                if (pref instanceof PreferenceScreen) {
+                    PreferenceScreen foundKey = findParentPreference((PreferenceScreen) pref,
+                            targetKey);
+                    if (foundKey != null) {
+                        return foundKey;
+                    }
+                } else if (pref.getKey() != null && pref.getKey().equals(targetKey)) {
+                    return parent;
+                }
+            }
+            return null;
+        }
+
         @Override
         public void onViewCreated(View view, Bundle savedInstanceState) {
             super.onViewCreated(view, savedInstanceState);
@@ -232,13 +289,14 @@
          * will remove that preference from the list.
          */
         protected boolean initPreference(Preference preference) {
+            DisplayController.Info info = DisplayController.INSTANCE.get(getContext()).getInfo();
             switch (preference.getKey()) {
                 case NOTIFICATION_DOTS_PREFERENCE_KEY:
                     return BuildConfig.NOTIFICATION_DOTS_ENABLED;
-
                 case ALLOW_ROTATION_PREFERENCE_KEY:
-                    DisplayController.Info info =
-                            DisplayController.INSTANCE.get(getContext()).getInfo();
+                    if (Flags.oneGridSpecs()) {
+                        return false;
+                    }
                     if (info.isTablet(info.realBounds)) {
                         // Launcher supports rotation by default. No need to show this setting.
                         return false;
@@ -246,14 +304,29 @@
                     // Initialize the UI once
                     preference.setDefaultValue(RotationHelper.getAllowRotationDefaultValue(info));
                     return true;
-
                 case DEVELOPER_OPTIONS_KEY:
                     if (IS_STUDIO_BUILD) {
                         preference.setOrder(0);
                     }
                     return mDeveloperOptionsEnabled;
+                case FIXED_LANDSCAPE_MODE:
+                    if (!Flags.oneGridSpecs()) {
+                        return false;
+                    }
+                    // When the setting changes rotate the screen accordingly to showcase the result
+                    // of the setting
+                    preference.setOnPreferenceChangeListener(
+                            (pref, newValue) -> {
+                                getActivity().setRequestedOrientation(
+                                        (boolean) newValue
+                                                ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+                                                : ActivityInfo.SCREEN_ORIENTATION_USER
+                                );
+                                return true;
+                            }
+                    );
+                    return !info.isTablet(info.realBounds);
             }
-
             return true;
         }
 
diff --git a/src/com/android/launcher3/shapes/AppShape.kt b/src/com/android/launcher3/shapes/AppShape.kt
new file mode 100644
index 0000000..68200a0
--- /dev/null
+++ b/src/com/android/launcher3/shapes/AppShape.kt
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+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 ac07c0f..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"));
         }
@@ -253,7 +258,7 @@
             if (mConfig.currentAnimation == null) {
                 // Run any queued runnable
                 if (listener != null) {
-                    listener.onAnimationEnd(null);
+                    listener.onAnimationEnd(new AnimatorSet());
                 }
                 return;
             } else if ((!mConfig.isUserControlled() && animated && mConfig.targetState == state)
@@ -282,7 +287,7 @@
 
             // Run any queued runnable
             if (listener != null) {
-                listener.onAnimationEnd(null);
+                listener.onAnimationEnd(new AnimatorSet());
             }
             return;
         }
@@ -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 db2a6e0..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,13 +489,13 @@
      */
     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);
     }
 
     /**
      * Returns the result by getting a generic property on UI thread
      */
-    private static <S, T> Bundle getUIProperty(
+    protected static <S, T> Bundle getUIProperty(
             BundleSetter<T> bundleSetter, Function<S, T> provider, Supplier<S> targetSupplier) {
         return getFromExecutorSync(MAIN_EXECUTOR, () -> {
             S target = targetSupplier.get();
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 3817563..74a0966 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -40,11 +40,13 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.FlingBlockCheck;
 import com.android.launcher3.util.TouchController;
+import com.android.systemui.contextualeducation.GestureType;
 
 /**
  * TouchController for handling state changes
@@ -388,6 +390,7 @@
         } else {
             logReachedState(mToState);
         }
+        updateContextualEduStats(targetState);
     }
 
     protected void goToTargetState(LauncherState targetState) {
@@ -403,6 +406,18 @@
                 .setDuration(0).start();
     }
 
+    private void updateContextualEduStats(LauncherState targetState) {
+        if (targetState == OVERVIEW) {
+            ContextualEduStatsManager.INSTANCE.get(
+                    mLauncher).updateEduStats(mDetector.isTrackpadGesture(), GestureType.OVERVIEW);
+        } else if (targetState == ALL_APPS && !mDetector.isTrackpadGesture()) {
+            // Only update if it is touch gesture as trackpad gesture is not relevant for all apps
+            // which only provides keyboard education.
+            ContextualEduStatsManager.INSTANCE.get(
+                    mLauncher).updateEduStats(/* isTrackpadGesture= */ false, GestureType.ALL_APPS);
+        }
+    }
+
     private void logReachedState(LauncherState targetState) {
         if (mStartState == targetState) {
             return;
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index fe4a83b..107bcc1 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -22,13 +22,9 @@
 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.app.animation.Interpolators.clampToProgress;
-import static com.android.app.animation.Interpolators.mapToProgress;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
-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_ALL_APPS_KEYBOARD_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
@@ -37,15 +33,12 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.states.StateAnimationConfig;
@@ -205,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);
@@ -247,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()) {
@@ -281,36 +274,6 @@
         }
     }
 
-    /**
-     * Applies Animation config values for transition from overview to all apps.
-     *
-     * @param threshold progress at which all apps will open upon release
-     */
-    public static void applyOverviewToAllAppsAnimConfig(
-            DeviceProfile deviceProfile, StateAnimationConfig config, float threshold) {
-        config.animProps |= StateAnimationConfig.USER_CONTROLLED;
-        config.animFlags = SKIP_OVERVIEW;
-        if (deviceProfile.isTablet) {
-            config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT);
-            config.setInterpolator(ANIM_SCRIM_FADE, ALL_APPS_SCRIM_RESPONDER);
-            // The fact that we end on Workspace is not very ideal, but since we do, fade it in at
-            // the end of the transition. Don't scale/translate it.
-            config.setInterpolator(ANIM_WORKSPACE_FADE, clampToProgress(LINEAR, 0.8f, 1));
-            config.setInterpolator(ANIM_WORKSPACE_SCALE, INSTANT);
-            config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, INSTANT);
-        } else {
-            // Pop the background panel, keyboard, and content in at full opacity at the threshold.
-            config.setInterpolator(ANIM_ALL_APPS_BOTTOM_SHEET_FADE,
-                    thresholdInterpolator(threshold, INSTANT));
-            config.setInterpolator(ANIM_ALL_APPS_KEYBOARD_FADE,
-                    thresholdInterpolator(threshold, INSTANT));
-            config.setInterpolator(ANIM_ALL_APPS_FADE, thresholdInterpolator(threshold, INSTANT));
-
-            config.setInterpolator(ANIM_VERTICAL_PROGRESS,
-                    thresholdInterpolator(threshold, mapToProgress(LINEAR, threshold, 1f)));
-        }
-    }
-
     /** Creates an interpolator that is 0 until the threshold, then follows given interpolator. */
     private static Interpolator thresholdInterpolator(float threshold, Interpolator interpolator) {
         return progress -> progress <= threshold ? 0 : interpolator.getInterpolation(progress);
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 52c3581..faac4a3 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -17,6 +17,8 @@
 
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
+
 import android.content.Context;
 import android.graphics.PointF;
 import android.util.Log;
@@ -64,6 +66,7 @@
     protected PointF mSubtractDisplacement = new PointF();
     @VisibleForTesting ScrollState mState = ScrollState.IDLE;
     private boolean mIsSettingState;
+    protected boolean mIsTrackpadGesture;
 
     protected boolean mIgnoreSlopWhenSettling;
     protected Context mContext;
@@ -122,6 +125,10 @@
         return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
     }
 
+    public boolean isTrackpadGesture() {
+        return mIsTrackpadGesture;
+    }
+
     public void finishedScrolling() {
         setState(ScrollState.IDLE);
     }
@@ -147,7 +154,7 @@
                 mLastPos.set(mDownPos);
                 mLastDisplacement.set(0, 0);
                 mDisplacement.set(0, 0);
-
+                mIsTrackpadGesture = isTrackpadMotionEvent(ev);
                 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
                     setState(ScrollState.DRAGGING);
                 }
diff --git a/src/com/android/launcher3/util/ActivityOptionsWrapper.java b/src/com/android/launcher3/util/ActivityOptionsWrapper.java
index 99cc1f7..17ff2a9 100644
--- a/src/com/android/launcher3/util/ActivityOptionsWrapper.java
+++ b/src/com/android/launcher3/util/ActivityOptionsWrapper.java
@@ -25,6 +25,7 @@
 public class ActivityOptionsWrapper {
 
     public final ActivityOptions options;
+    // Called when the app launch animation is complete
     public final RunnableList onEndCallback;
 
     public ActivityOptionsWrapper(ActivityOptions options, RunnableList onEndCallback) {
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..c729b4b
--- /dev/null
+++ b/src/com/android/launcher3/util/ContextTracker.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.views.ActivityContext;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Helper class to statically track activity creation
+ * @param <CONTEXT> The context type to track
+ */
+public abstract class ContextTracker<CONTEXT extends ActivityContext> {
+
+    private static final String TAG = "ContextTracker";
+
+    private WeakReference<CONTEXT> mCurrentContext = new WeakReference<>(null);
+    private CopyOnWriteArrayList<SchedulerCallback<CONTEXT>> mCallbacks =
+            new CopyOnWriteArrayList<>();
+
+    @Nullable
+    public <R extends CONTEXT> R getCreatedContext() {
+        return (R) mCurrentContext.get();
+    }
+
+    public void onContextDestroyed(CONTEXT context) {
+        if (mCurrentContext.get() == context) {
+            mCurrentContext.clear();
+        }
+    }
+
+    public abstract boolean isHomeStarted(CONTEXT context);
+
+    /**
+     * Call {@link SchedulerCallback#init(ActivityContext, boolean)} when the
+     * context is ready. If the context is already created, this is called immediately.
+     *
+     * The tracker maintains a strong ref to the callback, so it is up to the caller to return
+     * {@code false} in the callback OR to unregister the callback explicitly.
+     *
+     * @param callback The callback to call init() on when the context is ready.
+     */
+    public void registerCallback(SchedulerCallback<CONTEXT> callback, String reasonString) {
+        Log.d(TAG, "Registering callback: " + callback + ", reason=" + reasonString);
+        CONTEXT context = mCurrentContext.get();
+        mCallbacks.add(callback);
+        if (context != null) {
+            if (!callback.init(context, isHomeStarted(context))) {
+                unregisterCallback(callback, "ContextTracker.registerCallback: Intent handled");
+            }
+        }
+    }
+
+    /**
+     * Unregisters a registered callback.
+     */
+    public void unregisterCallback(SchedulerCallback<CONTEXT> callback, String reasonString) {
+        Log.d(TAG, "Unregistering callback: " + callback + ", reason=" + reasonString);
+        mCallbacks.remove(callback);
+    }
+
+    public boolean handleCreate(CONTEXT context) {
+        mCurrentContext = new WeakReference<>(context);
+        return handleCreate(context, /* alreadyOnHome= */ false);
+    }
+
+    public boolean handleNewIntent(CONTEXT context) {
+        return handleCreate(context, isHomeStarted(context));
+    }
+
+    private boolean handleCreate(CONTEXT context, boolean isHomeStarted) {
+        boolean handled = false;
+        if (!mCallbacks.isEmpty()) {
+            Log.d(TAG, "handleIntent: mCallbacks=" + mCallbacks);
+        }
+        for (SchedulerCallback<CONTEXT> cb : mCallbacks) {
+            if (!cb.init(context, isHomeStarted)) {
+                // Callback doesn't want any more updates
+                unregisterCallback(cb, "ContextTracker.handleIntent: Intent handled");
+            }
+            handled = true;
+        }
+        return handled;
+    }
+
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "ContextTracker:");
+        writer.println(prefix + "\tmCurrentContext=" + mCurrentContext.get());
+        writer.println(prefix + "\tmCallbacks=" + mCallbacks);
+    }
+
+    public interface SchedulerCallback<T extends ActivityContext> {
+
+        /**
+         * Called when the context is ready.
+         * @param isHomeStarted Whether the home activity is already started.
+         * @return Whether to continue receiving callbacks (i.e. if the context is recreated).
+         */
+        boolean init(T context, boolean isHomeStarted);
+    }
+
+    public static final class ActivityTracker<T extends BaseActivity> extends ContextTracker<T> {
+
+        @Override
+        public boolean isHomeStarted(T context) {
+            return context.isStarted();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/DaggerSingletonObject.java b/src/com/android/launcher3/util/DaggerSingletonObject.java
new file mode 100644
index 0000000..febe6af
--- /dev/null
+++ b/src/com/android/launcher3/util/DaggerSingletonObject.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import android.content.Context;
+
+import com.android.launcher3.LauncherApplication;
+import com.android.launcher3.dagger.LauncherAppComponent;
+
+import java.util.function.Function;
+
+/**
+ * A class to provide DaggerSingleton objects in a traditional way for
+ * {@link MainThreadInitializedObject}.
+ * We should delete this class at the end and use @Inject to get dagger provided singletons.
+ */
+
+public class DaggerSingletonObject<T> {
+    private final Function<LauncherAppComponent, T> mFunction;
+
+    public DaggerSingletonObject(Function<LauncherAppComponent, T> function) {
+        mFunction = function;
+    }
+
+    public T get(Context context) {
+        LauncherAppComponent component =
+                ((LauncherApplication) context.getApplicationContext()).getAppComponent();
+        return mFunction.apply(component);
+    }
+}
diff --git a/src/com/android/launcher3/util/DaggerSingletonTracker.java b/src/com/android/launcher3/util/DaggerSingletonTracker.java
new file mode 100644
index 0000000..b7a88db
--- /dev/null
+++ b/src/com/android/launcher3/util/DaggerSingletonTracker.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.util.Executors.MAIN_EXECUTOR;
+
+import com.android.launcher3.dagger.LauncherAppSingleton;
+
+import java.util.ArrayList;
+
+import javax.inject.Inject;
+
+/**
+ * A tracker class for keeping track of Dagger created singletons.
+ * Dagger will take care of creating singletons. But we should take care of unregistering callbacks
+ * if at all registered during singleton construction.
+ * All singletons should be declared as SafeCloseable so that we can call close() method.
+ */
+@LauncherAppSingleton
+public class DaggerSingletonTracker implements SafeCloseable {
+
+    private final ArrayList<SafeCloseable> mCloseables = new ArrayList<>();
+
+    private boolean mClosed = false;
+
+    @Inject
+    DaggerSingletonTracker() {
+    }
+
+    /**
+     * Adds the SafeCloseable Singletons to the mLauncherAppSingletons list.
+     * This helps to track the singletons and close them appropriately.
+     * See {@link DaggerSingletonTracker#close()} and
+     * {@link MainThreadInitializedObject.SandboxContext#onDestroy()}
+     */
+    public void addCloseable(SafeCloseable 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 = mCloseables.size() - 1; i >= 0; i--) {
+            mCloseables.get(i).close();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/DimensionUtils.kt b/src/com/android/launcher3/util/DimensionUtils.kt
index 63e919a..821dda7 100644
--- a/src/com/android/launcher3/util/DimensionUtils.kt
+++ b/src/com/android/launcher3/util/DimensionUtils.kt
@@ -31,7 +31,8 @@
     fun getTaskbarPhoneDimensions(
             deviceProfile: DeviceProfile,
             res: Resources,
-            isPhoneMode: Boolean
+            isPhoneMode: Boolean,
+            isGestureNav: Boolean,
     ): Point {
         val p = Point()
         // Taskbar for large screen
@@ -42,7 +43,7 @@
         }
 
         // Taskbar on phone using gesture nav, it will always be stashed
-        if (deviceProfile.isGestureMode) {
+        if (isGestureNav) {
             p.x = ViewGroup.LayoutParams.MATCH_PARENT
             p.y = res.getDimensionPixelSize(R.dimen.taskbar_stashed_size)
             return p
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 860f852..26912eb 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.util;
 
-import static android.content.Intent.ACTION_CONFIGURATION_CHANGED;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
@@ -33,16 +32,13 @@
 import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
 
 import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
 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;
 import android.hardware.display.DisplayManager;
-import android.os.Build;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
@@ -54,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;
@@ -77,6 +74,7 @@
 
     private static final String TAG = "DisplayController";
     private static final boolean DEBUG = false;
+    private static boolean sTaskbarModePreferenceStatusForTests = false;
     private static boolean sTransientTaskbarStatusForTests = true;
 
     // TODO(b/254119092) remove all logs with this tag
@@ -118,8 +116,7 @@
     private Info mInfo;
     private boolean mDestroyed = false;
 
-    private SharedPreferences.OnSharedPreferenceChangeListener
-            mTaskbarPinningPreferenceChangeListener;
+    private LauncherPrefChangeListener mTaskbarPinningPreferenceChangeListener;
 
     @VisibleForTesting
     protected DisplayController(Context context) {
@@ -131,38 +128,31 @@
         }
 
         Display display = mDM.getDisplay(DEFAULT_DISPLAY);
-        if (Utilities.ATLEAST_S) {
-            mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
-            mWindowContext.registerComponentCallbacks(this);
-        } else {
-            mWindowContext = null;
-            mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED);
-        }
+        mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
+        mWindowContext.registerComponentCallbacks(this);
 
         // Initialize navigation mode change listener
         mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
 
         WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context);
-        Context displayInfoContext = getDisplayInfoContext(display);
-        mInfo = new Info(displayInfoContext, wmProxy,
-                wmProxy.estimateInternalDisplayBounds(displayInfoContext));
+        mInfo = new Info(mWindowContext, wmProxy,
+                wmProxy.estimateInternalDisplayBounds(mWindowContext));
         FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds);
     }
 
     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) {
-                        handleInfoChange(mWindowContext.getDisplay());
-                    }
-                };
+        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);
@@ -186,18 +176,6 @@
         return INSTANCE.get(context).getInfo().isTransientTaskbar();
     }
 
-    /** Returns whether we are currently in Desktop mode. */
-    public static boolean isInDesktopMode(Context context) {
-        return INSTANCE.get(context).getInfo().isInDesktopMode();
-    }
-
-    /**
-     * Handles info change for desktop mode.
-     */
-    public static void handleInfoChangeForDesktopMode(Context context) {
-        INSTANCE.get(context).handleInfoChange(context.getDisplay());
-    }
-
     /**
      * Enables transient taskbar status for tests.
      */
@@ -207,12 +185,27 @@
     }
 
     /**
+     * Enables respecting taskbar mode preference during test.
+     */
+    @VisibleForTesting
+    public static void enableTaskbarModePreferenceForTests(boolean enable) {
+        sTaskbarModePreferenceStatusForTests = enable;
+    }
+
+    /**
      * Returns whether the taskbar is pinned in gesture navigation mode.
      */
     public static boolean isPinnedTaskbar(Context context) {
         return INSTANCE.get(context).getInfo().isPinnedTaskbar();
     }
 
+    /**
+     * Returns whether the taskbar is forced to be pinned when home is visible.
+     */
+    public static boolean showLockedTaskbarOnHome(Context context) {
+        return INSTANCE.get(context).getInfo().showLockedTaskbarOnHome();
+    }
+
     @Override
     public void close() {
         mDestroyed = true;
@@ -248,36 +241,24 @@
         if (mDestroyed) {
             return;
         }
-        boolean reconfigure = false;
         if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) {
-            reconfigure = true;
-        } else if (ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
-            Configuration config = mContext.getResources().getConfiguration();
-            reconfigure = mInfo.fontScale != config.fontScale
-                    || mInfo.densityDpi != config.densityDpi;
-        }
-
-        if (reconfigure) {
-            Log.d(TAG, "Configuration changed, notifying listeners");
-            Display display = mDM.getDisplay(DEFAULT_DISPLAY);
-            if (display != null) {
-                handleInfoChange(display);
-            }
+            Log.d(TAG, "Overlay changed, notifying listeners");
+            notifyConfigChange();
         }
     }
 
     @UiThread
     @Override
-    @TargetApi(Build.VERSION_CODES.S)
     public final void onConfigurationChanged(Configuration config) {
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config);
-        Display display = mWindowContext.getDisplay();
         if (config.densityDpi != mInfo.densityDpi
                 || config.fontScale != mInfo.fontScale
-                || display.getRotation() != mInfo.rotation
                 || !mInfo.mScreenSizeDp.equals(
-                        new PortraitSize(config.screenHeightDp, config.screenWidthDp))) {
-            handleInfoChange(display);
+                        new PortraitSize(config.screenHeightDp, config.screenWidthDp))
+                || mWindowContext.getDisplay().getRotation() != mInfo.rotation
+                || WindowManagerProxy.INSTANCE.get(mContext).showLockedTaskbarOnHome(mWindowContext)
+                        != mInfo.showLockedTaskbarOnHome()) {
+            notifyConfigChange();
         }
     }
 
@@ -300,17 +281,12 @@
         return mInfo;
     }
 
-    private Context getDisplayInfoContext(Display display) {
-        return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display);
-    }
-
     @AnyThread
-    @VisibleForTesting
-    public void handleInfoChange(Display display) {
+    public void notifyConfigChange() {
         WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(mContext);
         Info oldInfo = mInfo;
 
-        Context displayInfoContext = getDisplayInfoContext(display);
+        Context displayInfoContext = mWindowContext;
         Info newInfo = new Info(displayInfoContext, wmProxy, oldInfo.mPerDisplayBounds);
 
         if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale
@@ -341,7 +317,8 @@
         }
         if ((newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned)
                 || (newInfo.mIsTaskbarPinnedInDesktopMode
-                    != oldInfo.mIsTaskbarPinnedInDesktopMode)) {
+                    != oldInfo.mIsTaskbarPinnedInDesktopMode)
+                || newInfo.isPinnedTaskbar() != oldInfo.isPinnedTaskbar()) {
             change |= CHANGE_TASKBAR_PINNING;
         }
         if (newInfo.mIsInDesktopMode != oldInfo.mIsInDesktopMode) {
@@ -395,6 +372,9 @@
 
         private final boolean mIsInDesktopMode;
 
+        private final boolean mShowLockedTaskbarOnHome;
+        private final boolean mIsHomeVisible;
+
         public Info(Context displayInfoContext) {
             /* don't need system overrides for external displays */
             this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>());
@@ -456,6 +436,8 @@
             mIsTaskbarPinnedInDesktopMode = LauncherPrefs.get(displayInfoContext).get(
                     TASKBAR_PINNING_IN_DESKTOP_MODE);
             mIsInDesktopMode = wmProxy.isInDesktopMode();
+            mShowLockedTaskbarOnHome = wmProxy.showLockedTaskbarOnHome(displayInfoContext);
+            mIsHomeVisible = wmProxy.isHomeVisible(displayInfoContext);
         }
 
         /**
@@ -465,13 +447,17 @@
             if (navigationMode != NavigationMode.NO_BUTTON) {
                 return false;
             }
-            if (Utilities.isRunningInTestHarness()) {
+            if (Utilities.isRunningInTestHarness() && !sTaskbarModePreferenceStatusForTests) {
                 // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of
                 //  sTransientTaskbarStatusForTests and update test to directly
                 //  toggle shared preference to switch transient taskbar on/off.
                 return sTransientTaskbarStatusForTests;
             }
             if (enableTaskbarPinning()) {
+                // If Launcher is visible on the freeform display, ensure the taskbar is pinned.
+                if (mShowLockedTaskbarOnHome && mIsHomeVisible) {
+                    return false;
+                }
                 if (mIsInDesktopMode) {
                     return !mIsTaskbarPinnedInDesktopMode;
                 }
@@ -487,10 +473,6 @@
             return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar();
         }
 
-        public boolean isInDesktopMode() {
-            return mIsInDesktopMode;
-        }
-
         /**
          * Returns {@code true} if the bounds represent a tablet.
          */
@@ -543,6 +525,13 @@
                 return TYPE_PHONE;
             }
         }
+
+        /**
+         * Returns whether the taskbar is forced to be pinned when home is visible.
+         */
+        public boolean showLockedTaskbarOnHome() {
+            return mShowLockedTaskbarOnHome;
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/util/EdgeEffectCompat.java b/src/com/android/launcher3/util/EdgeEffectCompat.java
index ca37259..a949f50 100644
--- a/src/com/android/launcher3/util/EdgeEffectCompat.java
+++ b/src/com/android/launcher3/util/EdgeEffectCompat.java
@@ -19,8 +19,6 @@
 import android.view.MotionEvent;
 import android.widget.EdgeEffect;
 
-import com.android.launcher3.Utilities;
-
 /**
  * Extension of {@link EdgeEffect} to allow backwards compatibility
  */
@@ -30,21 +28,6 @@
         super(context);
     }
 
-    @Override
-    public float getDistance() {
-        return Utilities.ATLEAST_S ? super.getDistance() : 0;
-    }
-
-    @Override
-    public float onPullDistance(float deltaDistance, float displacement) {
-        if (Utilities.ATLEAST_S) {
-            return super.onPullDistance(deltaDistance, displacement);
-        } else {
-            onPull(deltaDistance, displacement);
-            return deltaDistance;
-        }
-    }
-
     public float onPullDistance(float deltaDistance, float displacement, MotionEvent ev) {
         return onPullDistance(deltaDistance, displacement);
     }
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 3d4b409..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.
@@ -76,4 +76,5 @@
      * When turned on, we enable zero state web data loader related logging.
      */
     public static final String ZERO_WEB_DATA_LOADER = "ZeroStateWebDataLoaderLog";
+    public static final String SEARCH_TARGET_UTIL_LOG = "SearchTargetUtilLog";
 }
diff --git a/src/com/android/launcher3/util/MSDLPlayerWrapper.java b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
new file mode 100644
index 0000000..1e53ac1
--- /dev/null
+++ b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.content.Context;
+import android.os.Vibrator;
+
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
+import com.google.android.msdl.data.model.MSDLToken;
+import com.google.android.msdl.domain.InteractionProperties;
+import com.google.android.msdl.domain.MSDLPlayer;
+
+import javax.inject.Inject;
+
+/**
+ * Wrapper around {@link com.google.android.msdl.domain.MSDLPlayer} to perform MSDL feedback.
+ */
+@LauncherAppSingleton
+public class MSDLPlayerWrapper {
+
+    public static final DaggerSingletonObject<MSDLPlayerWrapper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getMSDLPlayerWrapper);
+
+    /** Internal player */
+    private final MSDLPlayer mMSDLPlayer;
+
+    @Inject
+    public MSDLPlayerWrapper(@ApplicationContext Context context) {
+        Vibrator vibrator = context.getSystemService(Vibrator.class);
+        mMSDLPlayer = MSDLPlayer.Companion.createPlayer(vibrator, UI_HELPER_EXECUTOR, null);
+    }
+
+    /** Perform MSDL feedback for a token with interaction properties */
+    public void playToken(MSDLToken token, InteractionProperties properties) {
+        mMSDLPlayer.playToken(token, properties);
+    }
+
+    /** Perform MSDL feedback for a token without properties */
+    public void playToken(MSDLToken token) {
+        mMSDLPlayer.playToken(token, null);
+    }
+}
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index 1a0f9a0..356a551 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -18,13 +18,13 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
-import android.content.ContextWrapper;
 import android.os.Looper;
 import android.util.Log;
 
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.LauncherApplication;
 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
 
 import java.util.ArrayList;
@@ -35,6 +35,9 @@
 
 /**
  * Utility class for defining singletons which are initiated on main thread.
+ *
+ * TODO(b/361850561): Do not delete MainThreadInitializedObject until we find a way to
+ * unregister and understand how singleton objects are destroyed in dagger graph.
  */
 public class MainThreadInitializedObject<T extends SafeCloseable> {
 
@@ -47,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);
         }
 
@@ -97,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
@@ -105,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);
@@ -115,7 +138,7 @@
      * Abstract Context which allows custom implementations for
      * {@link MainThreadInitializedObject} providers
      */
-    public static class SandboxContext extends ContextWrapper implements SandboxApplication {
+    public static class SandboxContext extends LauncherApplication implements ObjectSandbox {
 
         private static final String TAG = "SandboxContext";
 
@@ -126,7 +149,7 @@
         private boolean mDestroyed = false;
 
         public SandboxContext(Context base) {
-            super(base);
+            attachBaseContext(base);
         }
 
         @Override
@@ -134,7 +157,20 @@
             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
                 for (int i = mOrderedObjects.size() - 1; i >= 0; i--) {
@@ -169,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/MultiTranslateDelegate.java b/src/com/android/launcher3/util/MultiTranslateDelegate.java
index 84ef445..38c87c8 100644
--- a/src/com/android/launcher3/util/MultiTranslateDelegate.java
+++ b/src/com/android/launcher3/util/MultiTranslateDelegate.java
@@ -37,6 +37,7 @@
     public static final int INDEX_TASKBAR_ALIGNMENT_ANIM = 3;
     public static final int INDEX_TASKBAR_REVEAL_ANIM = 4;
     public static final int INDEX_TASKBAR_PINNING_ANIM = 5;
+    public static final int INDEX_NAV_BAR_ANIM = 6;
 
     // Affect all items inside of a MultipageCellLayout
     public static final int INDEX_CELLAYOUT_MULTIPAGE_SPACING = 3;
@@ -47,7 +48,7 @@
     // Specific for hotseat items when adjusting for bubbles
     public static final int INDEX_BUBBLE_ADJUSTMENT_ANIM = 3;
 
-    public static final int COUNT = 6;
+    public static final int COUNT = 7;
 
     private final MultiPropertyFactory<View> mTranslationX;
     private final MultiPropertyFactory<View> mTranslationY;
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.kt b/src/com/android/launcher3/util/OnboardingPrefs.kt
index ac6e97c..771594e 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.kt
+++ b/src/com/android/launcher3/util/OnboardingPrefs.kt
@@ -16,6 +16,7 @@
 package com.android.launcher3.util
 
 import android.content.Context
+import androidx.annotation.VisibleForTesting
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
 
@@ -26,7 +27,7 @@
         val sharedPrefKey: String,
         val maxCount: Int,
     ) {
-        private val prefItem = backedUpItem(sharedPrefKey, 0)
+        @VisibleForTesting val prefItem = backedUpItem(sharedPrefKey, 0)
 
         /** @return The number of times we have seen the given event. */
         fun get(c: Context): Int {
diff --git a/src/com/android/launcher3/util/OverlayEdgeEffect.java b/src/com/android/launcher3/util/OverlayEdgeEffect.java
index d09d801..0623af7 100644
--- a/src/com/android/launcher3/util/OverlayEdgeEffect.java
+++ b/src/com/android/launcher3/util/OverlayEdgeEffect.java
@@ -46,6 +46,7 @@
         return mDistance;
     }
 
+    @Override
     public float onPullDistance(float deltaDistance, float displacement) {
         // Fallback implementation, will never actually get called
         if (BuildConfig.IS_DEBUG_DEVICE) {
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 8c5a76e..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,
@@ -212,7 +127,7 @@
         if (info instanceof ItemInfoWithIcon appInfo
                 && (appInfo.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
             context.startActivity(ApiWrapper.INSTANCE.get(context).getAppMarketActivityIntent(
-                    appInfo.getTargetComponent().getPackageName(), Process.myUserHandle()));
+                    appInfo.getTargetComponent().getPackageName(), Process.myUserHandle()), opts);
             return;
         }
         ComponentName componentName = null;
@@ -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
@@ -303,17 +189,7 @@
 
     /** Returns the incremental download progress for the given shortcut's app. */
     public static int getLoadingProgress(LauncherActivityInfo info) {
-        if (Utilities.ATLEAST_S) {
-            return (int) (100 * info.getLoadingProgress());
-        }
-        return 100;
-    }
-
-    /** 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);
+        return (int) (100 * info.getLoadingProgress());
     }
 
     /**
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 12eff61..50be98b 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -24,28 +24,51 @@
 import android.content.Context;
 import android.content.Intent;
 
+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 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 =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+    private final SimpleBroadcastReceiver mReceiver;
     private final CopyOnWriteArrayList<ScreenOnListener> mListeners = new CopyOnWriteArrayList<>();
 
     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(tracker);
+    }
+
+    @VisibleForTesting
+    ScreenOnTracker(@ApplicationContext Context context, SimpleBroadcastReceiver receiver,
+            DaggerSingletonTracker tracker) {
+        mContext = context;
+        mReceiver = receiver;
+        init(tracker);
+    }
+
+    private void init(DaggerSingletonTracker tracker) {
         mIsScreenOn = true;
-        mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+        mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+        tracker.addCloseable(this);
     }
 
     @Override
@@ -53,7 +76,8 @@
         mReceiver.unregisterReceiverSafely(mContext);
     }
 
-    private void onReceive(Intent intent) {
+    @VisibleForTesting
+    void onReceive(Intent intent) {
         String action = intent.getAction();
         if (ACTION_SCREEN_ON.equals(action)) {
             mIsScreenOn = true;
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/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index 07b7941..aa4f8af 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -54,14 +54,6 @@
                 ? ((WorkspaceItemInfo) info).getPersonKeys() : Utilities.EMPTY_STRING_ARRAY;
     }
 
-    /**
-     * Returns true if the item is a deep shortcut.
-     */
-    public static boolean isDeepShortcut(ItemInfo info) {
-        return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-                && info instanceof WorkspaceItemInfo;
-    }
-
     private static boolean isActive(ItemInfo info) {
         boolean isLoading = info instanceof WorkspaceItemInfo
                 && ((WorkspaceItemInfo) info).hasPromiseIconUi();
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index 95624b1..f457e4e 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -186,12 +186,6 @@
         public int stagePosition = STAGE_POSITION_UNDEFINED;
         @StageType
         public int stageType = STAGE_TYPE_UNDEFINED;
-
-        @Override
-        public String toString() {
-            return "SplitStageInfo { taskId=" + taskId
-                    + ", stagePosition=" + stagePosition + ", stageType=" + stageType + " }";
-        }
     }
 
     public static StatsLogManager.EventEnum getLogEventForPosition(@StagePosition int position) {
@@ -217,7 +211,7 @@
         private Drawable drawable;
         public final Intent intent;
         public final SplitPositionOption position;
-        public final ItemInfo itemInfo;
+        private ItemInfo itemInfo;
         public final StatsLogManager.EventEnum splitEvent;
         /** Represents the taskId of the first app to start in split screen */
         public int alreadyRunningTaskId = INVALID_TASK_ID;
@@ -245,5 +239,9 @@
         public View getView() {
             return view;
         }
+
+        public ItemInfo getItemInfo() {
+            return itemInfo;
+        }
     }
 }
diff --git a/src/com/android/launcher3/util/StableViewInfo.kt b/src/com/android/launcher3/util/StableViewInfo.kt
new file mode 100644
index 0000000..29dcd59
--- /dev/null
+++ b/src/com/android/launcher3/util/StableViewInfo.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.IBinder
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.ItemInfo.NO_ID
+
+/** Info parameters that can be used to identify a Launcher object */
+data class StableViewInfo(val itemId: Int, val containerId: Int, val stableId: Any) {
+
+    fun matches(info: ItemInfo?) =
+        info != null &&
+            itemId == info.id &&
+            containerId == info.container &&
+            stableId == info.stableId
+
+    companion object {
+
+        private fun ItemInfo.toStableViewInfo() =
+            stableId?.let { sId ->
+                if (id != NO_ID || container != NO_ID) StableViewInfo(id, container, sId) else null
+            }
+
+        /**
+         * Return a new launch cookie for the activity launch if supported.
+         *
+         * @param info the item info for the launch
+         */
+        @JvmStatic
+        fun toLaunchCookie(info: ItemInfo?) =
+            info?.toStableViewInfo()?.let { ObjectWrapper.wrap(it) }
+
+        /**
+         * Unwraps the binder and returns the first non-null StableViewInfo in the list or null if
+         * none can be found
+         */
+        @JvmStatic
+        fun fromLaunchCookies(launchCookies: List<IBinder>) =
+            launchCookies.firstNotNullOfOrNull { ObjectWrapper.unwrap<StableViewInfo>(it) }
+    }
+}
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/Themes.java b/src/com/android/launcher3/util/Themes.java
index 60951ba..104040a 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -52,10 +52,8 @@
     }
 
     public static int getActivityThemeRes(Context context, int wallpaperColorHints) {
-        boolean supportsDarkText = Utilities.ATLEAST_S
-                && (wallpaperColorHints & HINT_SUPPORTS_DARK_TEXT) != 0;
-        boolean isMainColorDark = Utilities.ATLEAST_S
-                && (wallpaperColorHints & HINT_SUPPORTS_DARK_THEME) != 0;
+        boolean supportsDarkText = (wallpaperColorHints & HINT_SUPPORTS_DARK_TEXT) != 0;
+        boolean isMainColorDark = (wallpaperColorHints & HINT_SUPPORTS_DARK_THEME) != 0;
 
         if (Utilities.isDarkTheme(context)) {
             return supportsDarkText ? R.style.AppTheme_Dark_DarkText
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 51749a7..39c9c42 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -19,28 +19,33 @@
 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;
 import android.content.Context;
 import android.media.AudioAttributes;
 import android.net.Uri;
-import android.os.SystemClock;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.provider.Settings;
 
-import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.Utilities;
+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)
@@ -49,123 +54,40 @@
 
     public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
-    private static final Uri HAPTIC_FEEDBACK_URI =
-            Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
+    @VisibleForTesting
+    static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
 
-    private static final float LOW_TICK_SCALE = 0.9f;
-    private static final float DRAG_TEXTURE_SCALE = 0.03f;
-    private static final float DRAG_COMMIT_SCALE = 0.5f;
-    private static final float DRAG_BUMP_SCALE = 0.4f;
-    private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
-
-    @Nullable
-    private final VibrationEffect mDragEffect;
-    @Nullable
-    private final VibrationEffect mCommitEffect;
-    @Nullable
-    private final VibrationEffect mBumpEffect;
-
-    private long mLastDragTime;
-    private final int mThresholdUntilNextDragCallMillis;
+    @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f;
 
     /**
      * Haptic when entering overview.
      */
     public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
 
-    private final Context mContext;
     private final Vibrator mVibrator;
     private final boolean mHasVibrator;
-    private final SettingsCache.OnChangeListener mHapticChangeListener =
+
+    @VisibleForTesting
+    final SettingsCache.OnChangeListener mHapticChangeListener =
             isEnabled -> mIsHapticFeedbackEnabled = isEnabled;
 
     private boolean mIsHapticFeedbackEnabled;
 
-    private VibratorWrapper(Context context) {
-        mContext = context;
+    @Inject
+    public VibratorWrapper(@ApplicationContext Context context, SettingsCache settingsCache,
+            DaggerSingletonTracker tracker) {
+
         mVibrator = context.getSystemService(Vibrator.class);
         mHasVibrator = mVibrator.hasVibrator();
         if (mHasVibrator) {
-            SettingsCache cache = SettingsCache.INSTANCE.get(mContext);
-            cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
-            mIsHapticFeedbackEnabled = cache.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;
         }
-
-        if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(
-                PRIMITIVE_LOW_TICK)) {
-
-            // Drag texture, Commit, and Bump should only be used for premium phones.
-            // Before using these haptics make sure check if the device can use it
-            VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
-            for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
-                dragEffect.addPrimitive(
-                        PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
-            }
-            mDragEffect = dragEffect.compose();
-            mCommitEffect = VibrationEffect.startComposition().addPrimitive(
-                    VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
-            mBumpEffect = VibrationEffect.startComposition().addPrimitive(
-                    PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose();
-            int primitiveDuration = mVibrator.getPrimitiveDurations(
-                    PRIMITIVE_LOW_TICK)[0];
-
-            mThresholdUntilNextDragCallMillis =
-                    DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100;
-        } else {
-            mDragEffect = null;
-            mCommitEffect = null;
-            mBumpEffect = null;
-            mThresholdUntilNextDragCallMillis = 0;
-        }
-    }
-
-    @Override
-    public void close() {
-        if (mHasVibrator) {
-            SettingsCache.INSTANCE.get(mContext)
-                    .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
-        }
-    }
-
-    /**
-     * This is called when the user swipes to/from all apps. This is meant to be used in between
-     * long animation progresses so that it gives a dragging texture effect. For a better
-     * experience, this should be used in combination with vibrateForDragCommit().
-     */
-    public void vibrateForDragTexture() {
-        if (mDragEffect == null) {
-            return;
-        }
-        long currentTime = SystemClock.elapsedRealtime();
-        long elapsedTimeSinceDrag = currentTime - mLastDragTime;
-        if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) {
-            vibrate(mDragEffect);
-            mLastDragTime = currentTime;
-        }
-    }
-
-    /**
-     * This is used when user reaches the commit threshold when swiping to/from from all apps.
-     */
-    public void vibrateForDragCommit() {
-        if (mCommitEffect != null) {
-            vibrate(mCommitEffect);
-        }
-        // resetting dragTexture timestamp to be able to play dragTexture again
-        mLastDragTime = 0;
-    }
-
-    /**
-     * The bump haptic is used to be called at the end of a swipe and only if it the gesture is a
-     * FLING going to/from all apps. Client can just call this method elsewhere just for the
-     * effect.
-     */
-    public void vibrateForDragBump() {
-        if (mBumpEffect != null) {
-            vibrate(mBumpEffect);
-        }
     }
 
     /**
@@ -174,8 +96,6 @@
      */
     public void cancelVibrate() {
         UI_HELPER_EXECUTOR.execute(mVibrator::cancel);
-        // reset dragTexture timestamp to be able to play dragTexture again whenever cancelled
-        mLastDragTime = 0;
     }
 
     /** Vibrates with the given effect if haptic feedback is available and enabled. */
@@ -206,7 +126,7 @@
 
     /** Indicates that Taskbar has been invoked. */
     public void vibrateForTaskbarUnstash() {
-        if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) {
+        if (mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) {
             VibrationEffect primitiveLowTickEffect = VibrationEffect
                     .startComposition()
                     .addPrimitive(PRIMITIVE_LOW_TICK, LOW_TICK_SCALE)
diff --git a/src/com/android/launcher3/util/ViewCache.java b/src/com/android/launcher3/util/ViewCache.java
index 98e6822..b98e977 100644
--- a/src/com/android/launcher3/util/ViewCache.java
+++ b/src/com/android/launcher3/util/ViewCache.java
@@ -21,6 +21,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.R;
 
 /**
@@ -67,7 +69,8 @@
         }
     }
 
-    private static class CacheEntry {
+    @VisibleForTesting
+    static class CacheEntry {
 
         final int mMaxSize;
         final View[] mViews;
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index e413d7f..2fa8bf4 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -24,6 +24,7 @@
 import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.util.ViewPool.Reusable;
 
@@ -43,9 +44,16 @@
 
     public ViewPool(Context context, @Nullable ViewGroup parent,
             int layoutId, int maxSize, int initialSize) {
+        this(LayoutInflater.from(context).cloneInContext(context),
+                parent, layoutId, maxSize, initialSize);
+    }
+
+    @VisibleForTesting
+    ViewPool(LayoutInflater inflater, @Nullable ViewGroup parent,
+            int layoutId, int maxSize, int initialSize) {
         mLayoutId = layoutId;
         mParent = parent;
-        mInflater = LayoutInflater.from(context);
+        mInflater = inflater;
         mPool = new Object[maxSize];
 
         if (initialSize > 0) {
diff --git a/src/com/android/launcher3/util/WallpaperColorHints.kt b/src/com/android/launcher3/util/WallpaperColorHints.kt
index 1361c1e..11d4c25 100644
--- a/src/com/android/launcher3/util/WallpaperColorHints.kt
+++ b/src/com/android/launcher3/util/WallpaperColorHints.kt
@@ -23,7 +23,6 @@
 import android.content.Context
 import androidx.annotation.MainThread
 import androidx.annotation.VisibleForTesting
-import com.android.launcher3.Utilities
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 
@@ -34,36 +33,34 @@
 class WallpaperColorHints(private val context: Context) : SafeCloseable {
     var hints: Int = 0
         private set
+
     private val wallpaperManager
         get() = context.getSystemService(WallpaperManager::class.java)!!
+
     private val onColorHintsChangedListeners = mutableListOf<OnColorHintListener>()
     private val onClose: SafeCloseable
 
     init {
-        if (Utilities.ATLEAST_S) {
-            hints = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)?.colorHints ?: 0
-            val onColorsChangedListener = OnColorsChangedListener { colors, which ->
-                onColorsChanged(colors, which)
-            }
+        hints = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)?.colorHints ?: 0
+        val onColorsChangedListener = OnColorsChangedListener { colors, which ->
+            onColorsChanged(colors, which)
+        }
+        UI_HELPER_EXECUTOR.execute {
+            wallpaperManager.addOnColorsChangedListener(
+                onColorsChangedListener,
+                MAIN_EXECUTOR.handler,
+            )
+        }
+        onClose = SafeCloseable {
             UI_HELPER_EXECUTOR.execute {
-                wallpaperManager.addOnColorsChangedListener(
-                    onColorsChangedListener,
-                    MAIN_EXECUTOR.handler
-                )
+                wallpaperManager.removeOnColorsChangedListener(onColorsChangedListener)
             }
-            onClose = SafeCloseable {
-                UI_HELPER_EXECUTOR.execute {
-                    wallpaperManager.removeOnColorsChangedListener(onColorsChangedListener)
-                }
-            }
-        } else {
-            onClose = SafeCloseable {}
         }
     }
 
     @MainThread
     private fun onColorsChanged(colors: WallpaperColors?, which: Int) {
-        if ((which and FLAG_SYSTEM) != 0 && Utilities.ATLEAST_S) {
+        if ((which and FLAG_SYSTEM) != 0) {
             val newHints = colors?.colorHints ?: 0
             if (newHints != hints) {
                 hints = newHints
@@ -86,6 +83,7 @@
         @VisibleForTesting
         @JvmField
         val INSTANCE = MainThreadInitializedObject { WallpaperColorHints(it) }
+
         @JvmStatic fun get(context: Context): WallpaperColorHints = INSTANCE.get(context)
     }
 }
diff --git a/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt b/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt
new file mode 100644
index 0000000..e9691a8
--- /dev/null
+++ b/src/com/android/launcher3/util/coroutines/DispatcherProvider.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.util.coroutines
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+
+interface DispatcherProvider {
+    val default: CoroutineDispatcher
+    val io: CoroutineDispatcher
+    val main: CoroutineDispatcher
+    val unconfined: CoroutineDispatcher
+}
+
+object ProductionDispatchers : DispatcherProvider {
+    override val default: CoroutineDispatcher = Dispatchers.Default
+    override val io: CoroutineDispatcher = Dispatchers.IO
+    override val main: CoroutineDispatcher = Dispatchers.Main
+    override val unconfined: CoroutineDispatcher = Dispatchers.Unconfined
+}
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/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 0817c0a..84b4a36 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -32,7 +32,6 @@
 import static com.android.launcher3.util.RotationUtils.rotateRect;
 import static com.android.launcher3.util.RotationUtils.rotateSize;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -40,7 +39,6 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
-import android.os.Build;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.Display;
@@ -54,7 +52,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.NavigationMode;
@@ -122,6 +119,20 @@
     }
 
     /**
+     * Returns if the pinned taskbar should be shown when home is visible.
+     */
+    public boolean showLockedTaskbarOnHome(Context displayInfoContext) {
+        return false;
+    }
+
+    /**
+     * Returns if the home is visible.
+     */
+    public boolean isHomeVisible(Context context) {
+        return false;
+    }
+
+    /**
      * Returns the real bounds for the provided display after applying any insets normalization
      */
     public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) {
@@ -216,7 +227,7 @@
             int screenWidthPx,
             @NonNull WindowInsets windowInsets,
             @NonNull WindowInsets.Builder insetsBuilder) {
-        if (!isLargeScreen || !Utilities.ATLEAST_S) {
+        if (!isLargeScreen) {
             return;
         }
 
@@ -391,25 +402,16 @@
     /**
      * Returns a CachedDisplayInfo initialized for the current display
      */
-    @TargetApi(Build.VERSION_CODES.S)
     public CachedDisplayInfo getDisplayInfo(Context displayInfoContext) {
         int rotation = getRotation(displayInfoContext);
-        if (Utilities.ATLEAST_S) {
-            WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class)
-                    .getMaximumWindowMetrics();
-            return getDisplayInfo(windowMetrics, rotation);
-        } else {
-            Point size = new Point();
-            Display display = getDisplay(displayInfoContext);
-            display.getRealSize(size);
-            return new CachedDisplayInfo(size, rotation);
-        }
+        WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class)
+                .getMaximumWindowMetrics();
+        return getDisplayInfo(windowMetrics, rotation);
     }
 
     /**
      * Returns a CachedDisplayInfo initialized for the current display
      */
-    @TargetApi(Build.VERSION_CODES.S)
     protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) {
         Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom);
         return new CachedDisplayInfo(size, rotation,
@@ -478,8 +480,7 @@
                 }
             }
         }
-        return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON :
-                NavigationMode.THREE_BUTTONS;
+        return NavigationMode.NO_BUTTON;
     }
 
     @Override
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 cfac91a..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,11 +79,13 @@
 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;
 
 import java.util.List;
 
@@ -174,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() {
@@ -215,6 +237,11 @@
         return null;
     }
 
+    @Nullable
+    default SystemUiController getSystemUiController() {
+        return null;
+    }
+
     /**
      * Handler for actions taken on drop targets that require launcher
      */
@@ -266,6 +293,14 @@
         return null;
     }
 
+    /**
+     * Returns the {@link WidgetPickerDataProvider} that can be used to read widgets for display.
+     */
+    @Nullable
+    default WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return null;
+    }
+
     @Nullable
     default StringCache getStringCache() {
         return null;
@@ -382,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/BubbleTextHolder.java b/src/com/android/launcher3/views/BubbleTextHolder.java
index 84f8049..d2ae93b 100644
--- a/src/com/android/launcher3/views/BubbleTextHolder.java
+++ b/src/com/android/launcher3/views/BubbleTextHolder.java
@@ -20,7 +20,7 @@
 /**
  * Views that contain {@link BubbleTextView} should implement this interface.
  */
-public interface BubbleTextHolder extends IconLabelDotView {
+public interface BubbleTextHolder extends FloatingIconViewCompanion {
     BubbleTextView getBubbleText();
 
     @Override
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index bc66a33..392d9a7 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -19,11 +19,19 @@
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.content.Context;
-import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ImageSpan;
 import android.util.AttributeSet;
-import android.widget.TextView;
+import android.util.Log;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
@@ -45,22 +53,68 @@
 
     public DoubleShadowBubbleTextView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mShadowInfo = new ShadowInfo(context, attrs, defStyle);
-        setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0, mShadowInfo.ambientShadowColor);
+        mShadowInfo = ShadowInfo.Companion.fromContext(context, attrs, defStyle);
+        setShadowLayer(
+                mShadowInfo.getAmbientShadowBlur(),
+                0,
+                0,
+                mShadowInfo.getAmbientShadowColor()
+        );
+    }
+
+    @Override
+    public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) {
+        Drawable drawable = getContext().getDrawable(drawableId);
+        if (drawable == null) {
+            setText(text);
+            Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
+                    + ", will just set text instead.");
+            return;
+        }
+        drawable.setTint(getCurrentTextColor());
+        int textSize = Math.round(getTextSize());
+        ImageSpan imageSpan;
+        if (!skipDoubleShadow() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            drawable = getDoubleShadowDrawable(drawable, textSize);
+        }
+        drawable.setBounds(0, 0, textSize, textSize);
+        imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER);
+        // First space will be replaced with Drawable, second space is for space before text.
+        SpannableString spannable = new SpannableString("  " + text);
+        spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+        setText(spannable);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    private DoubleShadowIconDrawable getDoubleShadowDrawable(
+            @NonNull Drawable drawable, int textSize
+    ) {
+        // add some padding via inset to avoid shadow clipping
+        int iconInsetSize = getContext().getResources()
+                .getDimensionPixelSize(R.dimen.app_title_icon_shadow_inset);
+        return new DoubleShadowIconDrawable(
+                mShadowInfo,
+                drawable,
+                textSize,
+                iconInsetSize
+        );
     }
 
     @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 (mShadowInfo.skipDoubleShadow(this)) {
+        if (skipDoubleShadow()) {
             super.onDraw(canvas);
             return;
         }
         int alpha = Color.alpha(getCurrentTextColor());
 
         // We enhance the shadow by drawing the shadow twice
-        getPaint().setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0,
-                getTextShadowColor(mShadowInfo.ambientShadowColor, alpha));
+        getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+                getTextShadowColor(mShadowInfo.getAmbientShadowColor(), alpha));
 
         drawWithoutDot(canvas);
         canvas.save();
@@ -69,10 +123,10 @@
                 getScrollY() + getHeight());
 
         getPaint().setShadowLayer(
-                mShadowInfo.keyShadowBlur,
-                mShadowInfo.keyShadowOffsetX,
-                mShadowInfo.keyShadowOffsetY,
-                getTextShadowColor(mShadowInfo.keyShadowColor, alpha));
+                mShadowInfo.getKeyShadowBlur(),
+                mShadowInfo.getKeyShadowOffsetX(),
+                mShadowInfo.getKeyShadowOffsetY(),
+                getTextShadowColor(mShadowInfo.getKeyShadowColor(), alpha));
         drawWithoutDot(canvas);
         canvas.restore();
 
@@ -80,55 +134,30 @@
         drawRunningAppIndicatorIfNecessary(canvas);
     }
 
-    public static class ShadowInfo {
-        public final float ambientShadowBlur;
-        public final int ambientShadowColor;
-
-        public final float keyShadowBlur;
-        public final float keyShadowOffsetX;
-        public final float keyShadowOffsetY;
-        public final int keyShadowColor;
-
-        public ShadowInfo(Context c, AttributeSet attrs, int defStyle) {
-
-            TypedArray a = c.obtainStyledAttributes(
-                    attrs, R.styleable.ShadowInfo, defStyle, 0);
-
-            ambientShadowBlur = a.getDimensionPixelSize(
-                    R.styleable.ShadowInfo_ambientShadowBlur, 0);
-            ambientShadowColor = a.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0);
-
-            keyShadowBlur = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0);
-            keyShadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0);
-            keyShadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0);
-            keyShadowColor = a.getColor(R.styleable.ShadowInfo_keyShadowColor, 0);
-            a.recycle();
-        }
-
-        public boolean skipDoubleShadow(TextView textView) {
-            int textAlpha = Color.alpha(textView.getCurrentTextColor());
-            int keyShadowAlpha = Color.alpha(keyShadowColor);
-            int ambientShadowAlpha = Color.alpha(ambientShadowColor);
-            if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
-                textView.getPaint().clearShadowLayer();
-                return true;
-            } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
-                textView.getPaint().setShadowLayer(ambientShadowBlur, 0, 0,
-                        getTextShadowColor(ambientShadowColor, textAlpha));
-                return true;
-            } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
-                textView.getPaint().setShadowLayer(
-                        keyShadowBlur,
-                        keyShadowOffsetX,
-                        keyShadowOffsetY,
-                        getTextShadowColor(keyShadowColor, textAlpha));
-                return true;
-            } else {
-                return false;
-            }
+    private boolean skipDoubleShadow() {
+        int textAlpha = Color.alpha(getCurrentTextColor());
+        int keyShadowAlpha = Color.alpha(mShadowInfo.getKeyShadowColor());
+        int ambientShadowAlpha = Color.alpha(mShadowInfo.getAmbientShadowColor());
+        if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
+            getPaint().clearShadowLayer();
+            return true;
+        } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
+            getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+                    getTextShadowColor(mShadowInfo.getAmbientShadowColor(), textAlpha));
+            return true;
+        } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
+            getPaint().setShadowLayer(
+                    mShadowInfo.getKeyShadowBlur(),
+                    mShadowInfo.getKeyShadowOffsetX(),
+                    mShadowInfo.getKeyShadowOffsetY(),
+                    getTextShadowColor(mShadowInfo.getKeyShadowColor(), textAlpha));
+            return true;
+        } else {
+            return false;
         }
     }
 
+
     // Multiplies the alpha of shadowColor by textAlpha.
     private static int getTextShadowColor(int shadowColor, int textAlpha) {
         return setColorAlphaBound(shadowColor,
diff --git a/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
new file mode 100644
index 0000000..7ac7c94
--- /dev/null
+++ b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.views
+
+import android.content.res.ColorStateList
+import android.graphics.BlendMode
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.RenderEffect
+import android.graphics.RenderNode
+import android.graphics.Shader
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.InsetDrawable
+import android.os.Build.VERSION_CODES
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Launcher wrapper for Drawables to provide a double shadow effect. Currently for use with
+ * [DoubleShadowBubbleTextView] to provide a similar shadow to inline icons.
+ */
+@RequiresApi(VERSION_CODES.S)
+class DoubleShadowIconDrawable(
+    private val shadowInfo: ShadowInfo,
+    iconDrawable: Drawable,
+    private val iconSize: Int,
+    iconInsetSize: Int
+) : Drawable() {
+    private val mIconDrawable: InsetDrawable
+    private val mDoubleShadowNode: RenderNode?
+
+    init {
+        mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize)
+        mIconDrawable.setBounds(0, 0, iconSize, iconSize)
+        mDoubleShadowNode = createShadowRenderNode()
+    }
+
+    @VisibleForTesting
+    fun createShadowRenderNode(): RenderNode {
+        val renderNode = RenderNode("DoubleShadowNode")
+        renderNode.setPosition(0, 0, iconSize, iconSize)
+        // Create render effects
+        val ambientShadow =
+            createShadowRenderEffect(
+                shadowInfo.ambientShadowBlur,
+                0f,
+                0f,
+                Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+            )
+        val keyShadow =
+            createShadowRenderEffect(
+                shadowInfo.keyShadowBlur,
+                shadowInfo.keyShadowOffsetX,
+                shadowInfo.keyShadowOffsetY,
+                Color.alpha(shadowInfo.keyShadowColor).toFloat()
+            )
+        val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP)
+        renderNode.setRenderEffect(blend)
+        return renderNode
+    }
+
+    @VisibleForTesting
+    fun createShadowRenderEffect(
+        radius: Float,
+        offsetX: Float,
+        offsetY: Float,
+        alpha: Float
+    ): RenderEffect {
+        return RenderEffect.createColorFilterEffect(
+            PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY),
+            RenderEffect.createOffsetEffect(
+                offsetX,
+                offsetY,
+                RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
+            )
+        )
+    }
+
+    override fun draw(canvas: Canvas) {
+        if (canvas.isHardwareAccelerated && mDoubleShadowNode != null) {
+            if (!mDoubleShadowNode.hasDisplayList()) {
+                // Record render node if its display list is not recorded or discarded
+                // (which happens when it's no longer drawn by anything).
+                val recordingCanvas = mDoubleShadowNode.beginRecording()
+                mIconDrawable.draw(recordingCanvas)
+                mDoubleShadowNode.endRecording()
+            }
+            canvas.drawRenderNode(mDoubleShadowNode)
+        }
+        mIconDrawable.draw(canvas)
+    }
+
+    override fun getIntrinsicHeight() = iconSize
+
+    override fun getIntrinsicWidth() = iconSize
+
+    override fun getOpacity() = PixelFormat.TRANSPARENT
+
+    override fun setAlpha(alpha: Int) {
+        mIconDrawable.alpha = alpha
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        mIconDrawable.colorFilter = colorFilter
+    }
+
+    override fun setTint(color: Int) {
+        mIconDrawable.setTint(color)
+    }
+
+    override fun setTintList(tint: ColorStateList?) {
+        mIconDrawable.setTintList(tint)
+    }
+}
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 1e577be..6739387 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -22,7 +22,7 @@
 import static com.android.launcher3.Utilities.getFullDrawable;
 import static com.android.launcher3.Utilities.mapToRange;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
+import static com.android.launcher3.views.FloatingIconViewCompanion.setPropertiesVisible;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -175,8 +175,9 @@
                 mLauncher.getDeviceProfile(), taskViewDrawAlpha);
 
         if (mFadeOutView != null) {
-            // The alpha goes from 1 to 0 when progress is 0 and 0.33 respectively.
-            mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.33f, 0, 1, LINEAR)));
+            // The alpha goes from 1 to 0 when progress is 0 and 0.15 respectively.
+            // This value minimizes view display time while still allowing the view to fade out.
+            mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.15f, 0, 1, LINEAR)));
         }
     }
 
@@ -252,11 +253,14 @@
     public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
             RectF outRect, Rect outViewBounds) {
         boolean ignoreTransform = !isOpening;
-        if (v instanceof BubbleTextHolder) {
-            v = ((BubbleTextHolder) v).getBubbleText();
+        if (v instanceof DeepShortcutView dsv) {
+            v = dsv.getIconView();
             ignoreTransform = false;
-        } else if (v.getParent() instanceof DeepShortcutView) {
-            v = ((DeepShortcutView) v.getParent()).getIconView();
+        } else if (v.getParent() instanceof DeepShortcutView dsv) {
+            v = dsv.getIconView();
+            ignoreTransform = false;
+        } else if (v instanceof BubbleTextHolder bth) {
+            v = bth.getBubbleText();
             ignoreTransform = false;
         }
         if (v == null) {
@@ -297,10 +301,10 @@
 
         Drawable badge = null;
         if (info instanceof SystemShortcut) {
-            if (originalView instanceof ImageView) {
-                drawable = ((ImageView) originalView).getDrawable();
-            } else if (originalView instanceof DeepShortcutView) {
-                drawable = ((DeepShortcutView) originalView).getIconView().getBackground();
+            if (originalView instanceof ImageView iv) {
+                drawable = iv.getDrawable();
+            } else if (originalView instanceof DeepShortcutView dsv) {
+                drawable = dsv.getIconView().getBackground();
             } else {
                 drawable = originalView.getBackground();
             }
@@ -515,6 +519,10 @@
             // When closing an app, we want the item on the workspace to be invisible immediately
             updateViewsVisibility(false  /* isVisible */);
         }
+        if (mFadeOutView instanceof FloatingIconViewCompanion fivc) {
+            fivc.setForceHideDot(true);
+            fivc.setForceHideRing(true);
+        }
     }
 
     @Override
@@ -652,6 +660,10 @@
             if (view.mFadeOutView != null) {
                 view.mFadeOutView.setAlpha(1f);
             }
+            if (view.mFadeOutView instanceof FloatingIconViewCompanion fivc) {
+                fivc.setForceHideDot(false);
+                fivc.setForceHideRing(false);
+            }
 
             if (hideOriginal) {
                 view.updateViewsVisibility(true /* isVisible */);
@@ -673,10 +685,10 @@
 
     private void updateViewsVisibility(boolean isVisible) {
         if (mOriginalIcon != null) {
-            setIconAndDotVisible(mOriginalIcon, isVisible);
+            setPropertiesVisible(mOriginalIcon, isVisible);
         }
         if (mMatchVisibilityView != null) {
-            setIconAndDotVisible(mMatchVisibilityView, isVisible);
+            setPropertiesVisible(mMatchVisibilityView, isVisible);
         }
     }
 
diff --git a/src/com/android/launcher3/views/FloatingIconViewCompanion.java b/src/com/android/launcher3/views/FloatingIconViewCompanion.java
new file mode 100644
index 0000000..fc23903
--- /dev/null
+++ b/src/com/android/launcher3/views/FloatingIconViewCompanion.java
@@ -0,0 +1,43 @@
+/*
+ * 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.views;
+
+import android.view.View;
+
+/**
+ * A view that can be drawn (in some capacity) via) {@link FloatingIconView}.
+ * This interface allows us to hide certain properties of the view that the FloatingIconView
+ * cannot draw, which allows us to make a seamless handoff between the FloatingIconView and
+ * the companion view.
+ */
+public interface FloatingIconViewCompanion {
+    void setIconVisible(boolean visible);
+    void setForceHideDot(boolean hide);
+    default void setForceHideRing(boolean hide) {}
+
+    /**
+     * Sets the visibility of icon and dot of the view
+     */
+    static void setPropertiesVisible(View view, boolean visible) {
+        if (view instanceof FloatingIconViewCompanion) {
+            ((FloatingIconViewCompanion) view).setIconVisible(visible);
+            ((FloatingIconViewCompanion) view).setForceHideDot(!visible);
+            ((FloatingIconViewCompanion) view).setForceHideRing(!visible);
+        } else {
+            view.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
index cab7982..5f8e2c0 100644
--- a/src/com/android/launcher3/views/FloatingSurfaceView.java
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -15,9 +15,8 @@
  */
 package com.android.launcher3.views;
 
-import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView;
-import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
+import static com.android.launcher3.views.FloatingIconViewCompanion.setPropertiesVisible;
 
 import android.content.Context;
 import android.graphics.Canvas;
@@ -160,7 +159,7 @@
         if (mContract == null) {
             return;
         }
-        View icon = mLauncher.getFirstMatchForAppClose(NO_MATCHING_ID,
+        View icon = mLauncher.getFirstMatchForAppClose(null /* StableViewInfo */,
                 mContract.componentName.getPackageName(), mContract.user,
                 false /* supportsAllAppsState */);
 
@@ -237,7 +236,7 @@
 
     private void setCurrentIconVisible(boolean isVisible) {
         if (mIcon != null) {
-            setIconAndDotVisible(mIcon, isVisible);
+            setPropertiesVisible(mIcon, isVisible);
         }
     }
 }
diff --git a/src/com/android/launcher3/views/IconLabelDotView.java b/src/com/android/launcher3/views/IconLabelDotView.java
deleted file mode 100644
index e9113cf..0000000
--- a/src/com/android/launcher3/views/IconLabelDotView.java
+++ /dev/null
@@ -1,38 +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.views;
-
-import android.view.View;
-
-/**
- * A view that has an icon, label, and notification dot.
- */
-public interface IconLabelDotView {
-    void setIconVisible(boolean visible);
-    void setForceHideDot(boolean hide);
-
-    /**
-     * Sets the visibility of icon and dot of the view
-     */
-    static void setIconAndDotVisible(View view, boolean visible) {
-        if (view instanceof IconLabelDotView) {
-            ((IconLabelDotView) view).setIconVisible(visible);
-            ((IconLabelDotView) view).setForceHideDot(!visible);
-        } else {
-            view.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 62eed5c..82cc40d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.views;
 
-import static androidx.core.content.ContextCompat.getColorStateList;
-
 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
 import static com.android.launcher3.LauncherState.EDIT_MODE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
@@ -147,8 +145,7 @@
 
     @Override
     public void assignMarginsAndBackgrounds(ViewGroup viewGroup) {
-        assignMarginsAndBackgrounds(viewGroup,
-                getColorStateList(getContext(), mColorIds[0]).getDefaultColor());
+        assignMarginsAndBackgrounds(viewGroup, mColors[0]);
         // last shortcut doesn't need bottom margin
         final int count = viewGroup.getChildCount() - 1;
         for (int i = 0; i < count; i++) {
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index fa17b7b..6fd18be 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -20,6 +20,9 @@
 
 import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
 
+import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.ALL_APPS_SCROLLER;
+import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER;
+
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
@@ -40,11 +43,15 @@
 import android.view.WindowInsets;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.FastScrollRecyclerView;
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.LetterListTextView;
 import com.android.launcher3.graphics.FastScrollThumbDrawable;
 import com.android.launcher3.util.Themes;
 
@@ -55,6 +62,19 @@
  * The track and scrollbar that shows when you scroll the list.
  */
 public class RecyclerViewFastScroller extends View {
+
+    /** FastScrollerLocation describes what RecyclerView the fast scroller is dedicated to. */
+    public enum FastScrollerLocation {
+        UNKNOWN_SCROLLER(0),
+        ALL_APPS_SCROLLER(1),
+        WIDGET_SCROLLER(2);
+
+        public final int location;
+
+        FastScrollerLocation(int location) {
+            this.location = location;
+        }
+    }
     private static final String TAG = "RecyclerViewFastScroller";
     private static final boolean DEBUG = false;
     private static final int FASTSCROLL_THRESHOLD_MILLIS = 40;
@@ -106,6 +126,8 @@
     private final Point mThumbDrawOffset = new Point();
 
     private final Paint mTrackPaint;
+    private final int mThumbColor;
+    private final int mThumbLetterScrollerColor;
 
     private float mLastTouchY;
     private boolean mIsDragging;
@@ -139,6 +161,7 @@
     private int mDownX;
     private int mDownY;
     private int mLastY;
+    private FastScrollerLocation mFastScrollerLocation;
 
     public RecyclerViewFastScroller(Context context) {
         this(context, null);
@@ -151,13 +174,16 @@
     public RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
 
+        mFastScrollerLocation = FastScrollerLocation.UNKNOWN_SCROLLER;
         mTrackPaint = new Paint();
         mTrackPaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
 
+        mThumbColor = Themes.getColorAccent(context);
+        mThumbLetterScrollerColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright);
         mThumbPaint = new Paint();
         mThumbPaint.setAntiAlias(true);
-        mThumbPaint.setColor(Themes.getColorAccent(context));
+        mThumbPaint.setColor(mThumbColor);
         mThumbPaint.setStyle(Paint.Style.FILL);
 
         Resources res = getResources();
@@ -329,11 +355,26 @@
         if (!sectionName.equals(mPopupSectionName)) {
             mPopupSectionName = sectionName;
             mPopupView.setText(sectionName);
-            performHapticFeedback(CLOCK_TICK);
+            // AllApps haptics are taken care of by AllAppsFastScrollHelper.
+            if (mFastScrollerLocation != ALL_APPS_SCROLLER) {
+                performHapticFeedback(CLOCK_TICK);
+            }
         }
         animatePopupVisibility(!TextUtils.isEmpty(sectionName));
         mLastTouchY = boundedY;
         setThumbOffsetY((int) mLastTouchY);
+        updateFastScrollerLetterList(y);
+    }
+
+    private void updateFastScrollerLetterList(int y) {
+        if (!shouldUseLetterFastScroller()) {
+            return;
+        }
+        ConstraintLayout mLetterList = mRv.getLetterList();
+        for (int i = 0; i < mLetterList.getChildCount(); i++) {
+            LetterListTextView currentLetter = (LetterListTextView) mLetterList.getChildAt(i);
+            currentLetter.animateBasedOnYPosition(y + mTouchOffsetY);
+        }
     }
 
     /** End any active fast scrolling touch handling, if applicable. */
@@ -359,15 +400,35 @@
         mThumbDrawOffset.set(getWidth() / 2, mRv.getScrollBarTop());
         // Draw the track
         float halfW = mWidth / 2;
-        canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(),
-                mWidth, mWidth, mTrackPaint);
-
-        canvas.translate(0, mThumbOffsetY);
+        boolean useLetterFastScroller = shouldUseLetterFastScroller();
+        if (useLetterFastScroller) {
+            float translateX;
+            if (mIsDragging) {
+                // halfW * 3 is half circle.
+                translateX = halfW * 3;
+            } else {
+                translateX = halfW * 5;
+            }
+            canvas.translate(translateX, mThumbOffsetY);
+        } else {
+            canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(),
+                    mWidth, mWidth, mTrackPaint);
+            canvas.translate(0, mThumbOffsetY);
+        }
         mThumbDrawOffset.y += mThumbOffsetY;
+
+        /* Draw half circle */
         halfW += mThumbPadding;
         float r = getScrollThumbRadius();
-        mThumbBounds.set(-halfW, 0, halfW, mThumbHeight);
-        canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint);
+        if (useLetterFastScroller) {
+            mThumbPaint.setColor(mThumbLetterScrollerColor);
+            mThumbBounds.set(0, 0, 0, mThumbHeight);
+            canvas.drawCircle(-halfW, halfW, r * 2, mThumbPaint);
+        } else {
+            mThumbPaint.setColor(mThumbColor);
+            mThumbBounds.set(-halfW, 0, halfW, mThumbHeight);
+            canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint);
+        }
         mThumbBounds.roundOut(SYSTEM_GESTURE_EXCLUSION_RECT.get(0));
         // swiping very close to the thumb area (not just within it's bound)
         // will also prevent back gesture
@@ -380,6 +441,11 @@
         canvas.restoreToCount(saveCount);
     }
 
+    boolean shouldUseLetterFastScroller() {
+        return Flags.letterFastScroller()
+                && getScrollerLocation() == FastScrollerLocation.ALL_APPS_SCROLLER;
+    }
+
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mSystemGestureInsets = insets.getSystemGestureInsets();
@@ -421,19 +487,25 @@
         return isNearThumb(x, y);
     }
 
-    /**
-     * Returns whether the specified x position is near the scroll bar.
-     */
-    public boolean isNearScrollBar(int x) {
-        return x >= (getWidth() - mMaxWidth) / 2 - mScrollbarLeftOffsetTouchDelegate
-                && x <= (getWidth() + mMaxWidth) / 2;
+    public FastScrollerLocation getScrollerLocation() {
+        return mFastScrollerLocation;
+    }
+
+    public void setFastScrollerLocation(@NonNull FastScrollerLocation location) {
+        mFastScrollerLocation = location;
     }
 
     private void animatePopupVisibility(boolean visible) {
         if (mPopupVisible != visible) {
             mPopupVisible = visible;
-            mPopupView.animate().cancel();
-            mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
+            if (shouldUseLetterFastScroller()) {
+                mRv.getLetterList().animate().alpha(visible ? 1f : 0f)
+                        .setDuration(visible ? 200 : 150).start();
+            } else {
+                mPopupView.animate().cancel();
+                mPopupView.animate().alpha(visible ? 1f : 0f)
+                        .setDuration(visible ? 200 : 150).start();
+            }
         }
     }
 
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/ShadowInfo.kt b/src/com/android/launcher3/views/ShadowInfo.kt
new file mode 100644
index 0000000..4f626ec
--- /dev/null
+++ b/src/com/android/launcher3/views/ShadowInfo.kt
@@ -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.views
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.launcher3.R
+
+/**
+ * Launcher data holder for classes such as [DoubleShadowBubbleTextView] to model shadows for
+ * "double shadow" effect.
+ */
+data class ShadowInfo(
+    val ambientShadowBlur: Float,
+    val ambientShadowColor: Int,
+    val keyShadowBlur: Float,
+    val keyShadowOffsetX: Float,
+    val keyShadowOffsetY: Float,
+    val keyShadowColor: Int
+) {
+
+    companion object {
+        /** Constructs instance of ShadowInfo from Context and given attribute set. */
+        @JvmStatic
+        fun fromContext(context: Context, attrs: AttributeSet?, defStyle: Int): ShadowInfo {
+            val styledAttrs =
+                context.obtainStyledAttributes(attrs, R.styleable.ShadowInfo, defStyle, 0)
+            val shadowInfo =
+                ShadowInfo(
+                    ambientShadowBlur =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+                            .toFloat(),
+                    ambientShadowColor =
+                        styledAttrs.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0),
+                    keyShadowBlur =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+                            .toFloat(),
+                    keyShadowOffsetX =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+                            .toFloat(),
+                    keyShadowOffsetY =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+                            .toFloat(),
+                    keyShadowColor = styledAttrs.getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+                )
+            styledAttrs.recycle()
+            return shadowInfo
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java
index 923eb19..a13152e 100644
--- a/src/com/android/launcher3/views/SpringRelativeLayout.java
+++ b/src/com/android/launcher3/views/SpringRelativeLayout.java
@@ -25,8 +25,6 @@
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory;
 
-import com.android.launcher3.Utilities;
-
 /**
  * View group to allow rendering overscroll effect in a child at the parent level
  */
@@ -46,10 +44,8 @@
 
     public SpringRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mEdgeGlowTop = Utilities.ATLEAST_S
-                ? new EdgeEffect(context, attrs) : new EdgeEffect(context);
-        mEdgeGlowBottom = Utilities.ATLEAST_S
-                ? new EdgeEffect(context, attrs) : new EdgeEffect(context);
+        mEdgeGlowTop = new EdgeEffect(context, attrs);
+        mEdgeGlowBottom = new EdgeEffect(context, attrs);
         setWillNotDraw(false);
     }
 
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/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 104209e..12a14c2 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -104,13 +104,13 @@
 
     @UiThread
     private void enforceRoundedCorners() {
-        if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
+        if (mEnforcedCornerRadius <= 0) {
             resetRoundedCorners();
             return;
         }
         View background = RoundedCornerEnforcement.findBackground(this);
         if (background == null
-                || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+                || RoundedCornerEnforcement.hasAppWidgetOptedOut(background)) {
             resetRoundedCorners();
             return;
         }
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index c59e295..1c0d94c 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -45,13 +45,13 @@
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.views.AbstractSlideInView;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -60,7 +60,7 @@
  */
 public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
         implements OnClickListener, OnLongClickListener,
-        PopupDataProvider.PopupDataChangeListener, Insettable, OnDeviceProfileChangeListener {
+        WidgetPickerDataChangeListener, Insettable, OnDeviceProfileChangeListener {
     /** The default number of cells that can fit horizontally in a widget sheet. */
     public static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4;
 
@@ -106,14 +106,14 @@
         WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext())
                 .normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect());
         mNavBarScrimHeight = getNavBarScrimHeight(windowInsets);
-        mActivityContext.getPopupDataProvider().setChangeListener(this);
+        mActivityContext.getWidgetPickerDataProvider().setChangeListener(this);
         mActivityContext.addOnDeviceProfileChangeListener(this);
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mActivityContext.getPopupDataProvider().setChangeListener(null);
+        mActivityContext.getWidgetPickerDataProvider().setChangeListener(null);
         mActivityContext.removeOnDeviceProfileChangeListener(this);
     }
 
diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
index 2817299..e100157 100644
--- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
+++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
@@ -183,19 +183,14 @@
 
                 // Draw horizontal and vertical lines to represent individual columns.
                 final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
+                boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
+                        previewWidthF, /* bottom= */ previewHeightF);
 
-                if (Utilities.ATLEAST_S) {
-                    boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
-                            previewWidthF, /* bottom= */ previewHeightF);
-
-                    p.setStyle(Paint.Style.FILL);
-                    p.setColor(Color.WHITE);
-                    float roundedCorner = mContext.getResources().getDimension(
-                            android.R.dimen.system_app_widget_background_radius);
-                    c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
-                } else {
-                    boxRect = drawBoxWithShadow(c, previewWidthF, previewHeightF);
-                }
+                p.setStyle(Paint.Style.FILL);
+                p.setColor(Color.WHITE);
+                float roundedCorner = mContext.getResources().getDimension(
+                        android.R.dimen.system_app_widget_background_radius);
+                c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
 
                 p.setStyle(Paint.Style.STROKE);
                 p.setStrokeWidth(mContext.getResources()
@@ -218,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/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index 40c3984..91b899c 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -21,22 +21,17 @@
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.RemoteViews;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
-import java.util.WeakHashMap;
 import java.util.function.IntConsumer;
 
 /**
@@ -83,6 +78,11 @@
         mViewToRecycle = viewToRecycle;
     }
 
+    @VisibleForTesting
+    @Nullable ListenableHostView getViewToRecycle() {
+        return mViewToRecycle;
+    }
+
     @Override
     @NonNull
     public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
@@ -129,37 +129,4 @@
     public void clearViews() {
         super.clearViews();
     }
-
-    public static class ListenableHostView extends LauncherAppWidgetHostView {
-
-        private Set<Runnable> mUpdateListeners = Collections.EMPTY_SET;
-
-        ListenableHostView(Context context) {
-            super(context);
-        }
-
-        @Override
-        public void updateAppWidget(RemoteViews remoteViews) {
-            super.updateAppWidget(remoteViews);
-            mUpdateListeners.forEach(Runnable::run);
-        }
-
-        @Override
-        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-            super.onInitializeAccessibilityNodeInfo(info);
-            info.setClassName(LauncherAppWidgetHostView.class.getName());
-        }
-
-        /**
-         * Adds a callback to be run everytime the provided app widget updates.
-         * @return a closable to remove this callback
-         */
-        public SafeCloseable addUpdateListener(Runnable callback) {
-            if (mUpdateListeners == Collections.EMPTY_SET) {
-                mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>());
-            }
-            mUpdateListeners.add(callback);
-            return () -> mUpdateListeners.remove(callback);
-        }
-    }
 }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index 3e4fd8c..b877d7a 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -1,10 +1,9 @@
 package com.android.launcher3.widget;
 
-import static com.android.launcher3.Utilities.ATLEAST_S;
-
 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;
@@ -12,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;
 
 /**
@@ -25,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-";
 
@@ -68,6 +68,8 @@
 
     protected boolean mIsMinSizeFulfilled;
 
+    private PackageManager mPM;
+
     public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context,
             AppWidgetProviderInfo info) {
         final LauncherAppWidgetProviderInfo launcherInfo;
@@ -96,6 +98,7 @@
     }
 
     public void initSpans(Context context, InvariantDeviceProfile idp) {
+        mPM = context.getApplicationContext().getPackageManager();
         int minSpanX = 0;
         int minSpanY = 0;
         int maxSpanX = idp.numColumns;
@@ -103,7 +106,6 @@
         int spanX = 0;
         int spanY = 0;
 
-
         Point cellSize = new Point();
         for (DeviceProfile dp : idp.supportedProfiles) {
             dp.getCellSize(cellSize);
@@ -116,15 +118,13 @@
                     getSpanY(widgetPadding, minResizeHeight, dp.cellLayoutBorderSpacePx.y,
                             cellSize.y));
 
-            if (ATLEAST_S) {
-                if (maxResizeWidth > 0) {
-                    maxSpanX = Math.min(maxSpanX, getSpanX(widgetPadding, maxResizeWidth,
-                            dp.cellLayoutBorderSpacePx.x, cellSize.x));
-                }
-                if (maxResizeHeight > 0) {
-                    maxSpanY = Math.min(maxSpanY, getSpanY(widgetPadding, maxResizeHeight,
-                            dp.cellLayoutBorderSpacePx.y, cellSize.y));
-                }
+            if (maxResizeWidth > 0) {
+                maxSpanX = Math.min(maxSpanX, getSpanX(widgetPadding, maxResizeWidth,
+                        dp.cellLayoutBorderSpacePx.x, cellSize.x));
+            }
+            if (maxResizeHeight > 0) {
+                maxSpanY = Math.min(maxSpanY, getSpanY(widgetPadding, maxResizeHeight,
+                        dp.cellLayoutBorderSpacePx.y, cellSize.y));
             }
 
             spanX = Math.max(spanX,
@@ -135,18 +135,16 @@
                             cellSize.y));
         }
 
-        if (ATLEAST_S) {
-            // Ensures maxSpan >= minSpan
-            maxSpanX = Math.max(maxSpanX, minSpanX);
-            maxSpanY = Math.max(maxSpanY, minSpanY);
+        // Ensures maxSpan >= minSpan
+        maxSpanX = Math.max(maxSpanX, minSpanX);
+        maxSpanY = Math.max(maxSpanY, minSpanY);
 
-            // Use targetCellWidth/Height if it is within the min/max ranges.
-            // Otherwise, use the span of minWidth/Height.
-            if (targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX
-                    && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {
-                spanX = targetCellWidth;
-                spanY = targetCellHeight;
-            }
+        // Use targetCellWidth/Height if it is within the min/max ranges.
+        // Otherwise, use the span of minWidth/Height.
+        if (targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX
+                && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {
+            spanX = targetCellWidth;
+            spanY = targetCellHeight;
         }
 
         // If minSpanX/Y > spanX/Y, ignore the minSpanX/Y to match the behavior described in
@@ -191,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() {
@@ -213,8 +212,7 @@
     }
 
     public boolean isConfigurationOptional() {
-        return ATLEAST_S
-                && isReconfigurable()
+        return isReconfigurable()
                 && (getWidgetFeatures() & WIDGET_FEATURE_CONFIGURATION_OPTIONAL) != 0;
     }
 
@@ -229,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/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 1fb8c83..f499fca 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -37,6 +37,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseActivity;
@@ -49,7 +50,6 @@
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
@@ -484,7 +484,8 @@
      * Sets or unsets a flag the can change whether the widget host should be in the listening
      * state.
      */
-    private void setShouldListenFlag(int flag, boolean on) {
+    @VisibleForTesting
+    void setShouldListenFlag(int flag, boolean on) {
         if (on) {
             mFlags.updateAndGet(old -> old | flag);
         } else {
diff --git a/src/com/android/launcher3/widget/ListenableHostView.java b/src/com/android/launcher3/widget/ListenableHostView.java
new file mode 100644
index 0000000..b809db0
--- /dev/null
+++ b/src/com/android/launcher3/widget/ListenableHostView.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget;
+
+import android.content.Context;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.RemoteViews;
+
+import com.android.launcher3.util.SafeCloseable;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+public class ListenableHostView extends LauncherAppWidgetHostView {
+
+    private Set<Runnable> mUpdateListeners = Collections.EMPTY_SET;
+
+    ListenableHostView(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void updateAppWidget(RemoteViews remoteViews) {
+        super.updateAppWidget(remoteViews);
+        mUpdateListeners.forEach(Runnable::run);
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setClassName(LauncherAppWidgetHostView.class.getName());
+    }
+
+    /**
+     * Adds a callback to be run everytime the provided app widget updates.
+     * @return a closable to remove this callback
+     */
+    public SafeCloseable addUpdateListener(Runnable callback) {
+        if (mUpdateListeners == Collections.EMPTY_SET) {
+            mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>());
+        }
+        mUpdateListeners.add(callback);
+        return () -> mUpdateListeners.remove(callback);
+    }
+}
diff --git a/src/com/android/launcher3/widget/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/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 8857774..130d533 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -136,9 +136,7 @@
                 Drawable p = new FastBitmapDrawable(new DatabaseWidgetPreviewLoader(launcher)
                         .generateWidgetPreview(
                                 createWidgetInfo.info, maxWidth, previewSizeBeforeScale));
-                if (RoundedCornerEnforcement.isRoundedCornerEnabled()) {
-                    p = new RoundDrawableWrapper(p, mEnforcedRoundedCornersForWidget);
-                }
+                p = new RoundDrawableWrapper(p, mEnforcedRoundedCornersForWidget);
                 preview = p;
             }
 
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index 1e46ffd..e190dc3 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.Flags.enforceSystemRadiusForAppWidgets;
+
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
 import android.content.res.Resources;
@@ -28,8 +30,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -67,15 +67,10 @@
     /**
      * Check whether the app widget has opted out of the enforcement.
      */
-    public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
+    public static boolean hasAppWidgetOptedOut(@NonNull View background) {
         return background.getId() == android.R.id.background && background.getClipToOutline();
     }
 
-    /** Check if the app widget is in the deny list. */
-    public static boolean isRoundedCornerEnabled() {
-        return Utilities.ATLEAST_S && FeatureFlags.ENABLE_ENFORCED_ROUNDED_CORNERS.get();
-    }
-
     /**
      * Computes the rounded rectangle needed for this app widget.
      *
@@ -102,11 +97,12 @@
      * in the given context.
      */
     public static float computeEnforcedRadius(@NonNull Context context) {
-        if (!Utilities.ATLEAST_S) {
-            return 0;
-        }
         Resources res = context.getResources();
         float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius);
+        if (enforceSystemRadiusForAppWidgets()) {
+            return systemRadius;
+        }
+
         float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius);
         return Math.min(defaultRadius, systemRadius);
     }
diff --git a/src/com/android/launcher3/widget/WidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java
index f332054..352c0a3 100644
--- a/src/com/android/launcher3/widget/WidgetImageView.java
+++ b/src/com/android/launcher3/widget/WidgetImageView.java
@@ -24,6 +24,8 @@
 import android.util.AttributeSet;
 import android.view.View;
 
+import com.android.launcher3.icons.RoundDrawableWrapper;
+
 /**
  * View that draws a bitmap horizontally centered. If the image width is greater than the view
  * width, the image is scaled down appropriately.
@@ -85,6 +87,11 @@
         final float scale = bitmapAspectRatio > containerAspectRatio ? myWidth / bitmapWidth
                 : myHeight / bitmapHeight;
 
+        // When updating the scale, also update scale on drawable if it has rounding.
+        if (mDrawable instanceof RoundDrawableWrapper && scale <= 1) {
+            ((RoundDrawableWrapper) mDrawable).setCornerRadiusScale(scale);
+        }
+
         final float scaledWidth = bitmapWidth * scale;
         final float scaledHeight = bitmapHeight * scale;
 
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index 9132b4f..23d0585 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -32,6 +32,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -57,8 +58,13 @@
     final Context mContext;
 
     public WidgetManagerHelper(Context context) {
+        this(context, AppWidgetManager.getInstance(context));
+    }
+
+    @VisibleForTesting
+    public WidgetManagerHelper(Context context, AppWidgetManager appWidgetManager) {
         mContext = context;
-        mAppWidgetManager = AppWidgetManager.getInstance(context);
+        mAppWidgetManager = appWidgetManager;
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 894099d..ddbd291 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.widget;
 
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser;
 
 import android.content.Context;
 import android.graphics.Rect;
@@ -40,6 +41,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
 import com.android.launcher3.widget.util.WidgetsTableUtils;
 
 import java.util.List;
@@ -124,10 +126,10 @@
 
     @Override
     public void onWidgetsBound() {
-        List<WidgetItem> widgets = mActivityContext.getPopupDataProvider().getWidgetsForPackageUser(
-                new PackageUserKey(
-                        mOriginalItemInfo.getTargetComponent().getPackageName(),
-                        mOriginalItemInfo.user));
+        final WidgetPickerData data = mActivityContext.getWidgetPickerDataProvider().get();
+        final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(mOriginalItemInfo);
+        List<WidgetItem> widgets = packageUserKey != null ? findAllWidgetsForPackageUser(data,
+                packageUserKey) : List.of();
 
         TableLayout widgetsTable = findViewById(R.id.widgets_table);
         widgetsTable.removeAllViews();
@@ -247,4 +249,7 @@
             }
         }
     }
+
+    @Override
+    public void onRecommendedWidgetsBound() {} // no op
 }
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 398b1df..82a6883 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -18,10 +18,11 @@
 
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -52,6 +53,9 @@
         }
     }
 
+    @VisibleForTesting
+    CustomAppWidgetProviderInfo() {}
+
     @Override
     public void initSpans(Context context, InvariantDeviceProfile idp) {
         mIsMinSizeFulfilled = Math.min(spanX, minSpanX) <= idp.numColumns
@@ -59,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 50012b3..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;
@@ -30,10 +31,14 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+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;
@@ -45,31 +50,46 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
-import java.util.function.Consumer;
+import java.util.Map;
+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) {
+    @Inject
+    CustomWidgetManager(@ApplicationContext Context context, PluginManagerWrapper pluginManager,
+            DaggerSingletonTracker tracker) {
+        this(context, pluginManager, AppWidgetManager.getInstance(context), tracker);
+    }
+
+    @VisibleForTesting
+    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)) {
@@ -77,47 +97,49 @@
                     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 = AppWidgetManager.getInstance(context)
-                .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
+    @NonNull
+    Map<ComponentName, CustomWidgetPlugin> getPlugins() {
+        return mPlugins;
     }
 
     /**
      * 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);
     }
 
     /**
@@ -126,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);
+        }
     }
 
     /**
@@ -143,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;
     }
 
@@ -161,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/WidgetsListBaseEntriesBuilder.kt b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
new file mode 100644
index 0000000..1abe4da
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.content.Context
+import com.android.launcher3.compat.AlphabeticIndexCompat
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import java.util.function.Predicate
+
+/**
+ * A helper class that builds the list of [WidgetsListBaseEntry]s used by the UI to display widgets.
+ */
+class WidgetsListBaseEntriesBuilder(val context: Context) {
+
+    /** Builds the widgets list entries in a format understandable by the widget picking UI. */
+    @JvmOverloads
+    fun build(
+        widgetsByPackageItem: Map<PackageItemInfo, List<WidgetItem>>,
+        widgetFilter: Predicate<WidgetItem> = Predicate<WidgetItem> { true },
+    ): List<WidgetsListBaseEntry> {
+        val indexer = AlphabeticIndexCompat(context)
+
+        return buildList {
+            for ((pkgItem, widgetItems) in widgetsByPackageItem.entries) {
+                val filteredWidgetItems = widgetItems.filter { widgetFilter.test(it) }
+                if (filteredWidgetItems.isNotEmpty()) {
+                    // Enables fast scroll popup to show right characters in all locales.
+                    val sectionName = pkgItem.title?.let { indexer.computeSectionName(it) } ?: ""
+
+                    add(WidgetsListHeaderEntry.create(pkgItem, sectionName, filteredWidgetItems))
+                    add(WidgetsListContentEntry(pkgItem, sectionName, filteredWidgetItems))
+                }
+            }
+        }
+    }
+}
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/WidgetRecommendationCategory.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
index 072d1d5..a68effd 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
@@ -19,6 +19,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 
+import com.android.launcher3.R;
+
 import java.util.Objects;
 
 /**
@@ -26,6 +28,10 @@
  * option in the pop-up opened on long press of launcher workspace).
  */
 public class WidgetRecommendationCategory implements Comparable<WidgetRecommendationCategory> {
+    public static WidgetRecommendationCategory DEFAULT_WIDGET_RECOMMENDATION_CATEGORY =
+            new WidgetRecommendationCategory(
+                    R.string.others_widget_recommendation_category_label, /*order=*/0);
+
     /** Resource id that holds the user friendly label for the category. */
     @StringRes
     public final int categoryTitleRes;
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 2e36583..8bebfb2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -16,10 +16,15 @@
 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;
@@ -67,12 +72,15 @@
 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;
 import com.android.launcher3.workprofile.PersonalWorkPagedView;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -84,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;
 
@@ -117,7 +126,7 @@
                     WidgetsRecyclerView searchRecyclerView =
                             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
                     if (mIsInSearchMode && searchRecyclerView != null) {
-                        searchRecyclerView.bindFastScrollbar(mFastScroller);
+                        searchRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
                     }
                 }
 
@@ -135,14 +144,16 @@
     private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
     @Nullable
     PersonalWorkPagedView mViewPager;
-    private boolean mIsInSearchMode;
+    protected boolean mIsInSearchMode;
     private boolean mIsNoWidgetsViewNeeded;
     @Px
     protected int mMaxSpanPerRow;
     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;
@@ -217,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));
@@ -245,8 +260,18 @@
         mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
         mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
 
-        mSearchBar.initialize(
-                mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
+        mSearchBar.initialize(new WidgetsSearchDataProvider() {
+            @Override
+            public List<WidgetsListBaseEntry> getWidgets() {
+                if (enableTieredWidgetsByDefaultInPicker()) {
+                    // search all
+                    return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
+                } else {
+                    // Can be removed when inlining enableTieredWidgetsByDefaultInPicker flag
+                    return getWidgetsToDisplay();
+                }
+            }
+        }, /* searchModeListener= */ this);
     }
 
     private void setDeviceManagementResources() {
@@ -270,24 +295,26 @@
     }
 
     private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
-        recyclerView.bindFastScrollbar(mFastScroller);
+        recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
         if (mCurrentWidgetsRecyclerView != recyclerView) {
             // 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);
+            }
         }
     }
 
     protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
         // The first item is always an empty space entry. Look for any more items.
         boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
-        adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
 
         if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) {
             mNoWidgetsView.setText(R.string.no_search_results);
+            adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
         } else if (adapterHolder.mAdapterType == AdapterHolder.WORK
                 && mUserCache.getUserProfiles().stream()
                 .filter(userHandle -> mUserCache.getUserInfo(userHandle).isWork())
@@ -306,7 +333,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
@@ -462,22 +491,42 @@
         setTranslationShift(mTranslationShift);
     }
 
+    /**
+     * 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();
+    }
+
     @Override
     public void onWidgetsBound() {
         if (mIsInSearchMode) {
             return;
         }
-        List<WidgetsListBaseEntry> allWidgets =
-                mActivityContext.getPopupDataProvider().getAllWidgets();
+        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(allWidgets);
+        primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets);
 
         if (mHasWorkProfile) {
             mViewPager.setVisibility(VISIBLE);
             mTabBar.setVisibility(VISIBLE);
             AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
-            workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+            workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets);
             onActivePageChanged(mViewPager.getCurrentPage());
         } else {
             onActivePageChanged(0);
@@ -495,6 +544,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);
@@ -544,9 +603,14 @@
             mNoWidgetsView.setVisibility(GONE);
         } else {
             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
-            // Visibility of recommended widgets, recycler views and headers are handled in methods
-            // below.
-            post(this::onRecommendedWidgetsBound);
+            mAdapters.get(getCurrentAdapterHolderType()).mWidgetsRecyclerView.setVisibility(
+                    VISIBLE);
+            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();
         }
     }
@@ -561,12 +625,11 @@
         if (mIsInSearchMode) {
             return;
         }
-
         if (enableCategorizedWidgetSuggestions()) {
             // We avoid applying new recommendations when some are already displayed.
             if (mRecommendedWidgetsMap.isEmpty()) {
                 mRecommendedWidgetsMap =
-                        mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets();
+                        mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
             }
             mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
                     mRecommendedWidgetsMap,
@@ -578,17 +641,20 @@
             );
         } else {
             if (mRecommendedWidgets.isEmpty()) {
-                mRecommendedWidgets =
-                        mActivityContext.getPopupDataProvider().getRecommendedWidgets();
+                mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get()
+                        .getRecommendations()
+                        .values().stream()
+                        .flatMap(Collection::stream).toList();
+                mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
+                        mRecommendedWidgets,
+                        mDeviceProfile,
+                        /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+                        /* availableWidth= */ mMaxSpanPerRow,
+                        /* cellPadding= */ mWidgetCellHorizontalPadding
+                );
             }
-            mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
-                    mRecommendedWidgets,
-                    mDeviceProfile,
-                    /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
-                    /* availableWidth= */ mMaxSpanPerRow,
-                    /* cellPadding= */ mWidgetCellHorizontalPadding
-            );
         }
+
         mWidgetRecommendationsContainer.setVisibility(
                 mRecommendedWidgetsCount > 0 ? VISIBLE : GONE);
     }
@@ -805,7 +871,7 @@
                 + marginLayoutParams.topMargin;
     }
 
-    private int getCurrentAdapterHolderType() {
+    protected int getCurrentAdapterHolderType() {
         if (mIsInSearchMode) {
             return SEARCH;
         } else if (mViewPager != null) {
@@ -834,6 +900,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();
@@ -849,6 +916,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.
@@ -1018,6 +1096,7 @@
                     this::getEmptySpaceHeight,
                     /* iconClickListener= */ WidgetsFullSheet.this,
                     /* iconLongClickListener= */ WidgetsFullSheet.this,
+                    /* expandButtonClickListener= */ WidgetsFullSheet.this,
                     isTwoPane());
             mWidgetsListAdapter.setHasStableIds(true);
             switch (mAdapterType) {
@@ -1034,7 +1113,7 @@
         }
 
         private int getEmptySpaceHeight() {
-            return mSearchScrollView.getHeaderHeight();
+            return mStickyHeaderLayout != null ? mStickyHeaderLayout.getHeaderHeight() : 0;
         }
 
         void setup(WidgetsRecyclerView recyclerView) {
@@ -1043,7 +1122,7 @@
             mWidgetsRecyclerView.setClipToOutline(true);
             mWidgetsRecyclerView.setClipChildren(false);
             mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
-            mWidgetsRecyclerView.bindFastScrollbar(mFastScroller);
+            mWidgetsRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
             mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator);
             mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
             if (!isTwoPane()) {
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 c84680d..f9bd5f1 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -16,18 +16,23 @@
 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;
 import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
 import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
 import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Process;
+import android.os.UserHandle;
 import android.util.AttributeSet;
+import android.view.Gravity;
 import android.view.LayoutInflater;
+import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -35,6 +40,7 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
+import android.widget.PopupMenu;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
@@ -83,6 +89,17 @@
     private PackageUserKey mSelectedHeader;
     private TextView mHeaderDescription;
 
+    /**
+     * A menu displayed for options (e.g. "show all widgets" filter) around widget lists in the
+     * picker.
+     */
+    protected View mWidgetOptionsMenu;
+    /**
+     * State of the options in the menu (if displayed to the user).
+     */
+    @Nullable
+    protected WidgetOptionsMenuState mWidgetOptionsMenuState = null;
+
     public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
     }
@@ -130,6 +147,11 @@
         mHeaderTitle = mContent.findViewById(R.id.title);
         mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
 
+        mWidgetOptionsMenu = mContent.findViewById(R.id.widget_picker_widget_options_menu);
+        if (!enableTieredWidgetsByDefaultInPicker()) {
+            setupWidgetOptionsMenu();
+        }
+
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
         mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
@@ -155,6 +177,40 @@
         }
     }
 
+    protected void setupWidgetOptionsMenu() {
+        mWidgetOptionsMenu.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mWidgetOptionsMenuState != null) {
+                    PopupMenu popupMenu = new PopupMenu(mActivityContext, /*anchor=*/ v,
+                            Gravity.END);
+                    MenuItem menuItem = popupMenu.getMenu().add(
+                            R.string.widget_picker_show_all_widgets_menu_item_title);
+                    menuItem.setCheckable(true);
+                    menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
+                    menuItem.setOnMenuItemClickListener(
+                            item -> onShowAllWidgetsMenuItemClick(item));
+                    popupMenu.show();
+                }
+            }
+        });
+    }
+
+    private boolean onShowAllWidgetsMenuItemClick(MenuItem menuItem) {
+        mWidgetOptionsMenuState.showAllWidgets = !mWidgetOptionsMenuState.showAllWidgets;
+        menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
+
+        // Refresh widgets
+        onWidgetsBound();
+        if (mIsInSearchMode) {
+            mSearchBar.reset();
+        } else if (!mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)) {
+            mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
+            mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop();
+        }
+        return true;
+    }
+
     @Override
     protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
         if (enableCategorizedWidgetSuggestions()) {
@@ -233,6 +289,32 @@
         }
     }
 
+    // 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 =
+                mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
+        List<WidgetsListBaseEntry> defaultWidgets =
+                mActivityContext.getWidgetPickerDataProvider().get().getDefaultWidgets();
+
+        if (allWidgets.isEmpty() || defaultWidgets.isEmpty()) {
+            // no menu if there are no default widgets to show
+            mWidgetOptionsMenuState = null;
+            mWidgetOptionsMenu.setVisibility(GONE);
+        } else {
+            if (mWidgetOptionsMenuState == null) {
+                mWidgetOptionsMenuState = new WidgetOptionsMenuState();
+            }
+
+            mWidgetOptionsMenu.setVisibility(VISIBLE);
+            return mWidgetOptionsMenuState.showAllWidgets ? allWidgets : defaultWidgets;
+        }
+
+        return allWidgets;
+    }
+
     @Override
     public void onWidgetsBound() {
         super.onWidgetsBound();
@@ -243,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();
 
@@ -267,14 +358,9 @@
                 false);
         mSuggestedWidgetsHeader.setExpanded(true);
 
-        PackageItemInfo packageItemInfo = new PackageItemInfo(
+        PackageItemInfo packageItemInfo = new HighresPackageItemInfo(
                 /* packageName= */ SUGGESTIONS_PACKAGE_NAME,
-                Process.myUserHandle()) {
-            @Override
-            public boolean usingLowResIcon() {
-                return false;
-            }
-        };
+                Process.myUserHandle());
         String suggestionsHeaderTitle = getContext().getString(
                 R.string.suggested_widgets_header_title);
         String suggestionsRightPaneTitle = getContext().getString(
@@ -285,7 +371,7 @@
         WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create(
                         packageItemInfo,
                         /*titleSectionName=*/ suggestionsHeaderTitle,
-                        /*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+                        /*items=*/ List.of(), // not necessary
                         /*visibleWidgetsCount=*/ 0)
                 .withWidgetListShown();
 
@@ -301,17 +387,14 @@
             mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo);
             final boolean isChangingHeaders = mSelectedHeader == null
                     || !mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey);
-            // If the initial focus view is still focused, it is likely a programmatic header
-            // click.
-            if (mSelectedHeader != null
-                    && !getAccessibilityInitialFocusView().isAccessibilityFocused()) {
-                post(() -> {
-                    mRightPaneScrollView.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
-                    mRightPaneScrollView.performAccessibilityAction(
-                            AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
-                });
-            }
             if (isChangingHeaders)  {
+                // If the initial focus view is still focused or widget picker is still opening, it
+                // is likely a programmatic header click.
+                if (mSelectedHeader != null && !mOpenCloseAnimation.getAnimationPlayer().isRunning()
+                        && !getAccessibilityInitialFocusView().isAccessibilityFocused()) {
+                    mRightPaneScrollView.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
+                    focusOnFirstWidgetCell(mWidgetRecommendationsView);
+                }
                 // If switching from another header, unselect any WidgetCells. This is necessary
                 // because we do not clear/recycle the WidgetCells in the recommendations container
                 // when the header is clicked, only when onRecommendationsBound is called. That
@@ -389,6 +472,13 @@
         if (!isWidgetAvailable) {
             mRightPane.removeAllViews();
             mRightPane.addView(mNoWidgetsView);
+            // with no widgets message, no header is selected on left
+            if (mSuggestedWidgetsPackageUserKey != null
+                    && mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)
+                    && mSuggestedWidgetsHeader != null) {
+                mSuggestedWidgetsHeader.setExpanded(false);
+            }
+            mSelectedHeader = null;
         }
         super.updateRecyclerViewVisibility(adapterHolder);
     }
@@ -430,13 +520,26 @@
             public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) {
                 final boolean isSameHeader = mSelectedHeader != null
                         && mSelectedHeader.equals(selectedHeader);
-                // If the initial focus view is still focused, it is likely a programmatic header
-                // click.
+                // If the initial focus view is still focused or widget picker is still opening, it
+                // is likely a programmatic header click.
                 final boolean isUserClick = mSelectedHeader != null
+                        && !mOpenCloseAnimation.getAnimationPlayer().isRunning()
                         && !getAccessibilityInitialFocusView().isAccessibilityFocused();
                 mSelectedHeader = selectedHeader;
-                WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider()
-                        .getSelectedAppWidgets(selectedHeader);
+
+                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;
@@ -570,4 +673,26 @@
          */
         void onHeaderChanged(@NonNull PackageUserKey selectedHeader);
     }
+
+    /**
+     * Holds the selection state of the options menu (if presented to the user).
+     */
+    protected static class WidgetOptionsMenuState {
+        /**
+         * UI state indicating whether to show default or all widgets.
+         * <p>If true, shows all widgets; else shows the default widgets.</p>
+         */
+        public boolean showAllWidgets = false;
+    }
+
+    private static class HighresPackageItemInfo extends PackageItemInfo {
+        HighresPackageItemInfo(String packageName, UserHandle user) {
+            super(packageName, user);
+        }
+
+        @Override
+        public boolean usingLowResIcon() {
+            return false;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt
new file mode 100644
index 0000000..46d3e7a
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model
+
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets
+import java.io.PrintWriter
+
+/**
+ * Provides [WidgetPickerData] to various views such as widget picker, app-specific widget picker,
+ * widgets shortcut.
+ */
+class WidgetPickerDataProvider {
+    /** All the widgets data provided for the views */
+    private var mWidgetPickerData: WidgetPickerData = WidgetPickerData()
+
+    private var changeListener: WidgetPickerDataChangeListener? = null
+
+    /** Sets a listener to be called back when widget data is updated. */
+    fun setChangeListener(changeListener: WidgetPickerDataChangeListener?) {
+        this.changeListener = changeListener
+    }
+
+    /** Returns the current snapshot of [WidgetPickerData]. */
+    fun get(): WidgetPickerData {
+        return mWidgetPickerData
+    }
+
+    /**
+     * Updates the widgets available to the widget picker.
+     *
+     * Generally called when the widgets model has new data.
+     */
+    @JvmOverloads
+    fun setWidgets(
+        allWidgets: List<WidgetsListBaseEntry>,
+        defaultWidgets: List<WidgetsListBaseEntry> = listOf()
+    ) {
+        mWidgetPickerData =
+            mWidgetPickerData.withWidgets(allWidgets = allWidgets, defaultWidgets = defaultWidgets)
+        changeListener?.onWidgetsBound()
+    }
+
+    /**
+     * Makes the widget recommendations available to the widget picker
+     *
+     * Generally called when new widget predictions are available.
+     */
+    fun setWidgetRecommendations(recommendations: List<ItemInfo>) {
+        mWidgetPickerData = mWidgetPickerData.withRecommendedWidgets(recommendations)
+        changeListener?.onRecommendedWidgetsBound()
+    }
+
+    /** Writes the current state to the provided writer. */
+    fun dump(prefix: String, writer: PrintWriter) {
+        writer.println(prefix + "WidgetPickerDataProvider:")
+        writer.println("$prefix\twidgetPickerData:$mWidgetPickerData")
+    }
+
+    interface WidgetPickerDataChangeListener {
+        /** A callback to get notified when widgets are bound. */
+        fun onWidgetsBound()
+
+        /** A callback to get notified when recommended widgets are bound. */
+        fun onRecommendedWidgetsBound()
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt
new file mode 100644
index 0000000..3332ef0
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.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.widget.picker.model.data
+
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY
+
+// This file contains WidgetPickerData and utility functions to operate on it.
+
+/** Widget data for display in the widget picker. */
+data class WidgetPickerData(
+    val allWidgets: List<WidgetsListBaseEntry> = listOf(),
+    val defaultWidgets: List<WidgetsListBaseEntry> = listOf(),
+    val recommendations: Map<WidgetRecommendationCategory, List<WidgetItem>> = mapOf(),
+)
+
+/** Provides utility methods to work with a [WidgetPickerData] object. */
+object WidgetPickerDataUtils {
+    /**
+     * Returns a [WidgetPickerData] with the provided widgets.
+     *
+     * When [defaultWidgets] is not passed, defaults from previous object are not copied over.
+     * Defaults (if supported) should be updated when all widgets are updated.
+     */
+    fun WidgetPickerData.withWidgets(
+        allWidgets: List<WidgetsListBaseEntry>,
+        defaultWidgets: List<WidgetsListBaseEntry> = listOf()
+    ): WidgetPickerData {
+        return copy(allWidgets = allWidgets, defaultWidgets = defaultWidgets)
+    }
+
+    /** Returns a [WidgetPickerData] with the given recommendations set. */
+    fun WidgetPickerData.withRecommendedWidgets(recommendations: List<ItemInfo>): WidgetPickerData {
+        val allWidgetsMap: Map<ComponentKey, WidgetItem> =
+            allWidgets
+                .filterIsInstance<WidgetsListContentEntry>()
+                .flatMap { it.mWidgets }
+                .filterNotNull()
+                .distinct()
+                .associateBy { it } // as ComponentKey
+
+        val categoriesMap =
+            recommendations
+                .filterIsInstance<PendingAddWidgetInfo>()
+                .filter { allWidgetsMap.containsKey(ComponentKey(it.targetComponent, it.user)) }
+                .groupBy { it.recommendationCategory ?: DEFAULT_WIDGET_RECOMMENDATION_CATEGORY }
+                .mapValues { (_, pendingAddWidgetInfos) ->
+                    pendingAddWidgetInfos.map {
+                        allWidgetsMap[ComponentKey(it.targetComponent, it.user)] as WidgetItem
+                    }
+                }
+
+        return copy(recommendations = categoriesMap)
+    }
+
+    /** Finds all [WidgetItem]s available for the provided package user. */
+    @JvmStatic
+    fun findAllWidgetsForPackageUser(
+        widgetPickerData: WidgetPickerData,
+        packageUserKey: PackageUserKey
+    ): List<WidgetItem> {
+        return findContentEntryForPackageUser(widgetPickerData, packageUserKey)?.mWidgets
+            ?: emptyList()
+    }
+
+    /**
+     * Finds and returns the [WidgetsListContentEntry] for the given package user.
+     *
+     * Set [fromDefaultWidgets] to true to limit the content entry to default widgets.
+     */
+    @JvmOverloads
+    @JvmStatic
+    fun findContentEntryForPackageUser(
+        widgetPickerData: WidgetPickerData,
+        packageUserKey: PackageUserKey,
+        fromDefaultWidgets: Boolean = false
+    ): WidgetsListContentEntry? {
+        val widgetsListBaseEntries =
+            if (fromDefaultWidgets) {
+                widgetPickerData.defaultWidgets
+            } else {
+                widgetPickerData.allWidgets
+            }
+
+        return widgetsListBaseEntries.filterIsInstance<WidgetsListContentEntry>().firstOrNull {
+            PackageUserKey.fromPackageItemInfo(it.mPkgItem) == packageUserKey
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
index 65937b6..92caf3e 100644
--- a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
+++ b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
@@ -26,7 +26,6 @@
 
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.R;
-import com.android.launcher3.popup.PopupDataProvider;
 
 /**
  * View for a search bar with an edit text with a cancel button.
@@ -51,7 +50,8 @@
     }
 
     @Override
-    public void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener) {
+    public void initialize(WidgetsSearchDataProvider dataProvider,
+            SearchModeListener searchModeListener) {
         mController = new WidgetsSearchBarController(
                 new SimpleWidgetsSearchAlgorithm(dataProvider),
                 mEditText, mCancelButton, searchModeListener);
diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
index 613066a..0e88787 100644
--- a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
+++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
@@ -21,13 +21,13 @@
 import android.os.Handler;
 
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.search.SearchAlgorithm;
 import com.android.launcher3.search.SearchCallback;
 import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -39,9 +39,9 @@
 public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm<WidgetsListBaseEntry> {
 
     private final Handler mResultHandler;
-    private final PopupDataProvider mDataProvider;
+    private final WidgetsSearchDataProvider mDataProvider;
 
-    public SimpleWidgetsSearchAlgorithm(PopupDataProvider dataProvider) {
+    public SimpleWidgetsSearchAlgorithm(WidgetsSearchDataProvider dataProvider) {
         mResultHandler = new Handler();
         mDataProvider = dataProvider;
     }
@@ -63,9 +63,9 @@
      * Returns entries for all matched widgets
      */
     public static ArrayList<WidgetsListBaseEntry> getFilteredWidgets(
-            PopupDataProvider dataProvider, String input) {
+            WidgetsSearchDataProvider dataProvider, String input) {
         ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
-        dataProvider.getAllWidgets().stream()
+        dataProvider.getWidgets().stream()
                 .filter(entry -> entry instanceof WidgetsListHeaderEntry)
                 .forEach(headerEntry -> {
                     List<WidgetItem> matchedWidgetItems = filterWidgetItems(
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
index 44a5e80..ab504e7 100644
--- a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
@@ -16,7 +16,9 @@
 
 package com.android.launcher3.widget.picker.search;
 
-import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
 
 /**
  * Interface for a widgets picker search bar.
@@ -25,7 +27,7 @@
     /**
      * Attaches a controller to the search bar which interacts with {@code searchModeListener}.
      */
-    void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener);
+    void initialize(WidgetsSearchDataProvider dataProvider, SearchModeListener searchModeListener);
 
     /**
      * Clears search bar.
@@ -44,4 +46,15 @@
      * Sets the vertical location, in pixels, of this search bar relative to its top position.
      */
     void setTranslationY(float translationY);
+
+
+    /**
+     * Provides corpus from which search results must be returned.
+     */
+    interface WidgetsSearchDataProvider {
+        /**
+         * Returns the widgets from which the search should return the results.
+         */
+        List<WidgetsListBaseEntry> getWidgets();
+    }
 }
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/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt
index a016676..1ab8f8b 100644
--- a/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt
+++ b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt
@@ -28,6 +28,7 @@
         WidgetPreviewContainerSize(spanX = 2, spanY = 3),
         WidgetPreviewContainerSize(spanX = 2, spanY = 2),
         WidgetPreviewContainerSize(spanX = 4, spanY = 1),
+        WidgetPreviewContainerSize(spanX = 3, spanY = 1),
         WidgetPreviewContainerSize(spanX = 2, spanY = 1),
         WidgetPreviewContainerSize(spanX = 1, spanY = 1),
     )
diff --git a/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java
new file mode 100644
index 0000000..63d87e8
--- /dev/null
+++ b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.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.dagger;
+
+import dagger.Component;
+
+/**
+ * Root component for Dagger injection for Launcher AOSP.
+ */
+@LauncherAppSingleton
+@Component
+public interface LauncherAppComponent extends LauncherBaseAppComponent {
+    /** Builder for aosp LauncherAppComponent. */
+    @Component.Builder
+    interface Builder extends LauncherBaseAppComponent.Builder {
+        LauncherAppComponent build();
+    }
+}
+
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/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
index a940774..eaa9ef0 100644
--- a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
@@ -49,6 +49,8 @@
 
     default void onActivityDestroyed() { }
 
+    default void onDisallowSwipeToMinusOnePage() {}
+
     /**
      * @deprecated use LauncherOverlayTouchProxy directly
      */
diff --git a/tests/Android.bp b/tests/Android.bp
index e51242f..35a2275 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -71,6 +71,9 @@
 // Library with all the dependencies for building quickstep
 android_library {
     name: "Launcher3TestLib",
+    defaults: [
+        "launcher_compose_tests_defaults",
+    ],
     srcs: [],
     asset_dirs: ["assets"],
     resource_dirs: ["res"],
@@ -95,6 +98,8 @@
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
         "android.appwidget.flags-aconfig-java",
+        "platform-parametric-runner-lib",
+        "kotlin-reflect",
     ],
     manifest: "AndroidManifest-common.xml",
     platform_apis: true,
@@ -108,10 +113,16 @@
     asset_dirs: ["assets"],
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
+    static_libs: [
+        "kotlin-reflect",
+    ],
 }
 
 android_test {
     name: "Launcher3Tests",
+    defaults: [
+        "launcher_compose_tests_defaults",
+    ],
     srcs: [
         ":launcher-tests-src",
         ":launcher-non-quickstep-tests-src",
@@ -121,9 +132,9 @@
         "com_android_launcher3_flags_lib",
     ],
     libs: [
-        "android.test.base",
-        "android.test.runner",
-        "android.test.mock",
+        "android.test.base.stubs.system",
+        "android.test.runner.stubs.system",
+        "android.test.mock.stubs.system",
     ],
     // Libraries used by mockito inline extended
     jni_libs: [
@@ -137,6 +148,7 @@
     platform_apis: true,
     test_config: "Launcher3Tests.xml",
     data: [":Launcher3"],
+    plugins: ["dagger2-compiler"],
     test_suites: ["general-tests"],
 }
 
@@ -154,7 +166,7 @@
 }
 
 filegroup {
-    name: "launcher-testing-helpers",
+    name: "launcher-testing-helpers-robo",
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
@@ -168,20 +180,26 @@
         // Test classes
         "src/**/*Test.java",
         "src/**/*Test.kt",
+        "src/**/RoboApiWrapper.kt",
         "multivalentTests/src/**/*Test.java",
         "multivalentTests/src/**/*Test.kt",
     ],
 }
 
+filegroup {
+    name: "launcher-testing-helpers",
+    srcs: [
+        ":launcher-testing-helpers-robo",
+        "src/**/RoboApiWrapper.kt",
+    ],
+}
+
 android_robolectric_test {
     enabled: true,
     name: "Launcher3RoboTests",
     srcs: [
         ":launcher3-robo-src",
-
-        // Test util classes
-        ":launcher-testing-helpers",
-        ":launcher-testing-shared",
+        ":launcher-testing-helpers-robo",
     ],
     exclude_srcs: [
         //"src/com/android/launcher3/util/CellContentDimensionsTest.kt", // Failing - b/316553889
@@ -214,12 +232,13 @@
         "android.appwidget.flags-aconfig-java",
     ],
     libs: [
-        "android.test.runner",
-        "android.test.base",
-        "android.test.mock",
+        "android.test.runner.stubs.system",
+        "android.test.base.stubs.system",
+        "android.test.mock.stubs.system",
         "truth",
     ],
     instrumentation_for: "Launcher3",
+    plugins: ["dagger2-compiler"],
     upstream: true,
     strict_mode: false,
 }
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 4b926a8..1ddd453 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -407,6 +407,36 @@
             </intent-filter>
         </activity>
 
+        <activity-alias android:name="AppIconActivity"
+            android:label="Application Icon"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="DiffIconActivity"
+            android:label="Different icon"
+            android:exported="true"
+            android:icon="@drawable/test_different_activity_icon"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="WrongIconActivity"
+            android:label="Wrong icon"
+            android:exported="true"
+            android:icon="@drawable/test_wrong_activity_icon"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+
         <!-- Disable eager initialization of Jetpack libraries. See bug 197780098. -->
         <provider
             android:name="androidx.startup.InitializationProvider"
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
index 82a6310..4c366c3 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
index 4271105..6db9534 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 147.0px (56.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
index 8bd6b99..6e76b13 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 0.0px (0.0dp)
 	mHotseatBarEdgePaddingPx: 63.0px (24.0dp)
 	mHotseatBarWorkspaceSpacePx: 42.0px (16.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
index 8dbb413..1af9215 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 0.0px (0.0dp)
 	mHotseatBarEdgePaddingPx: 63.0px (24.0dp)
 	mHotseatBarWorkspaceSpacePx: 42.0px (16.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
index ab4b286..958597f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 80.0px (40.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
index 80835bc..aad67b4 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 80.0px (40.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
index fc53107..090e54b 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 152.0px (76.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
index 836819f..43b1a65 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 152.0px (76.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
index 108182f..fe5737e 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
index 313d2a3..36e47a0 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt
index 46cce24..52fea05 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt
index 44b99e9..6d972a8 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
index fb392a8..417353d 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
index 2c4b3c3..03dc23a 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt
index e7b72f2..45d3171 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt
index eae50f1..55322d6 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index fab3015..825b52b 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -148,16 +148,13 @@
 
     public static final String REQUEST_HOTSEAT_CELL_CENTER = "hotseat-cell-center";
 
-    public static final String REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET =
-            "get-focused-task-height-for-tablet";
-    public static final String REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET =
-            "get-grid-task-size-rect-for-tablet";
+    public static final String REQUEST_GET_OVERVIEW_TASK_SIZE = "get-overivew-task-size";
+    public static final String REQUEST_GET_OVERVIEW_GRID_TASK_SIZE = "get-overivew-grid-task-size";
     public static final String REQUEST_GET_OVERVIEW_PAGE_SPACING = "get-overview-page-spacing";
     public static final String REQUEST_GET_OVERVIEW_CURRENT_PAGE_INDEX =
             "get-overview-current-page-index";
     public static final String REQUEST_GET_SPLIT_SELECTION_ACTIVE = "get-split-selection-active";
     public static final String REQUEST_ENABLE_ROTATION = "enable_rotation";
-    public static final String REQUEST_ENABLE_SUGGESTION = "enable-suggestion";
     public static final String REQUEST_MODEL_QUEUE_CLEARED = "model-queue-cleared";
 
     public static boolean sDebugTracing = false;
@@ -170,12 +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 OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
-    public static final String PRIVATE_SPACE_SCROLL_FAILURE = "b/339737008";
-
     public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
     public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 8770859..f8794f8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -79,7 +79,7 @@
         val statusBarNaturalPx: Int,
         val statusBarRotatedPx: Int,
         val gesturePx: Int,
-        val cutoutPx: Int
+        val cutoutPx: Int,
     )
 
     open val deviceSpecs =
@@ -91,7 +91,7 @@
                     statusBarNaturalPx = 118,
                     statusBarRotatedPx = 74,
                     gesturePx = 63,
-                    cutoutPx = 118
+                    cutoutPx = 118,
                 ),
             "tablet" to
                 DeviceSpec(
@@ -100,7 +100,7 @@
                     statusBarNaturalPx = 104,
                     statusBarRotatedPx = 104,
                     gesturePx = 0,
-                    cutoutPx = 0
+                    cutoutPx = 0,
                 ),
             "twopanel-phone" to
                 DeviceSpec(
@@ -109,7 +109,7 @@
                     statusBarNaturalPx = 133,
                     statusBarRotatedPx = 110,
                     gesturePx = 63,
-                    cutoutPx = 133
+                    cutoutPx = 133,
                 ),
             "twopanel-tablet" to
                 DeviceSpec(
@@ -118,14 +118,15 @@
                     statusBarNaturalPx = 110,
                     statusBarRotatedPx = 133,
                     gesturePx = 0,
-                    cutoutPx = 0
-                )
+                    cutoutPx = 0,
+                ),
         )
 
     protected fun initializeVarsForPhone(
         deviceSpec: DeviceSpec,
         isGestureMode: Boolean = true,
-        isVerticalBar: Boolean = false
+        isVerticalBar: Boolean = false,
+        isFixedLandscape: Boolean = false,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
@@ -137,14 +138,15 @@
             displayInfo,
             rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0,
             isGestureMode,
-            densityDpi = deviceSpec.densityDpi
+            densityDpi = deviceSpec.densityDpi,
+            isFixedLandscape = isFixedLandscape,
         )
     }
 
     protected fun initializeVarsForTablet(
         deviceSpec: DeviceSpec,
         isLandscape: Boolean = false,
-        isGestureMode: Boolean = true
+        isGestureMode: Boolean = true,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
@@ -156,7 +158,7 @@
             displayInfo,
             rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
             isGestureMode,
-            densityDpi = deviceSpec.densityDpi
+            densityDpi = deviceSpec.densityDpi,
         )
     }
 
@@ -165,7 +167,8 @@
         deviceSpecFolded: DeviceSpec,
         isLandscape: Boolean = false,
         isGestureMode: Boolean = true,
-        isFolded: Boolean = false
+        isFolded: Boolean = false,
+        isFixedLandscape: Boolean = false,
     ) {
         val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
         val unfoldedWindowsBounds =
@@ -182,7 +185,7 @@
         val perDisplayBoundsCache =
             mapOf(
                 unfoldedDisplayInfo to unfoldedWindowsBounds,
-                foldedDisplayInfo to foldedWindowsBounds
+                foldedDisplayInfo to foldedWindowsBounds,
             )
 
         if (isFolded) {
@@ -191,7 +194,8 @@
                 displayInfo = foldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecFolded.densityDpi
+                densityDpi = deviceSpecFolded.densityDpi,
+                isFixedLandscape = isFixedLandscape,
             )
         } else {
             initializeCommonVars(
@@ -199,7 +203,8 @@
                 displayInfo = unfoldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecUnfolded.densityDpi
+                densityDpi = deviceSpecUnfolded.densityDpi,
+                isFixedLandscape = isFixedLandscape,
             )
         }
     }
@@ -208,7 +213,7 @@
         deviceSpec: DeviceSpec,
         isGestureMode: Boolean,
         naturalX: Int,
-        naturalY: Int
+        naturalY: Int,
     ): List<WindowBounds> {
         val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
 
@@ -217,14 +222,14 @@
                 0,
                 max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx),
                 0,
-                if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight
+                if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
             )
         val rotation90Insets =
             Rect(
                 deviceSpec.cutoutPx,
                 deviceSpec.statusBarRotatedPx,
                 if (isGestureMode) 0 else buttonsNavHeight,
-                if (isGestureMode) deviceSpec.gesturePx else 0
+                if (isGestureMode) deviceSpec.gesturePx else 0,
             )
         val rotation180Insets =
             Rect(
@@ -233,29 +238,29 @@
                 0,
                 max(
                     if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
-                    deviceSpec.cutoutPx
-                )
+                    deviceSpec.cutoutPx,
+                ),
             )
         val rotation270Insets =
             Rect(
                 if (isGestureMode) 0 else buttonsNavHeight,
                 deviceSpec.statusBarRotatedPx,
                 deviceSpec.cutoutPx,
-                if (isGestureMode) deviceSpec.gesturePx else 0
+                if (isGestureMode) deviceSpec.gesturePx else 0,
             )
 
         return listOf(
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180),
-            WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270)
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270),
         )
     }
 
     private fun tabletWindowsBounds(
         deviceSpec: DeviceSpec,
         naturalX: Int,
-        naturalY: Int
+        naturalY: Int,
     ): List<WindowBounds> {
         val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
         val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
@@ -264,7 +269,7 @@
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180),
-            WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270)
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270),
         )
     }
 
@@ -273,7 +278,8 @@
         displayInfo: CachedDisplayInfo,
         rotation: Int,
         isGestureMode: Boolean = true,
-        densityDpi: Int
+        densityDpi: Int,
+        isFixedLandscape: Boolean = false,
     ) {
         setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_TWOLINE_TOGGLE)
         LauncherPrefs.get(testContext).put(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE, true)
@@ -307,6 +313,11 @@
 
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING)).thenReturn(false)
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(true)
+        whenever(launcherPrefs.get(LauncherPrefs.FIXED_LANDSCAPE_MODE)).thenReturn(isFixedLandscape)
+        whenever(launcherPrefs.get(LauncherPrefs.HOTSEAT_COUNT)).thenReturn(-1)
+        whenever(launcherPrefs.get(LauncherPrefs.DEVICE_TYPE)).thenReturn(-1)
+        whenever(launcherPrefs.get(LauncherPrefs.WORKSPACE_SIZE)).thenReturn("")
+        whenever(launcherPrefs.get(LauncherPrefs.DB_FILE)).thenReturn("")
         val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
         whenever(displayController.info).thenReturn(info)
         whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
diff --git a/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
index 21abab4..0e06051 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
@@ -29,7 +29,7 @@
 
     @Before
     fun setup() {
-        launcherPrefs = LauncherPrefs(DeviceHelpers.context)
+        launcherPrefs = LauncherPrefs.get(DeviceHelpers.context)
         receiverUnderTest = AppWidgetsRestoredReceiver()
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
index b04bcca..f73a9d3 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
@@ -41,6 +41,8 @@
 import com.android.launcher3.LauncherSettings.Favorites.SPANX
 import com.android.launcher3.LauncherSettings.Favorites.SPANY
 import com.android.launcher3.LauncherSettings.Favorites._ID
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.util.ApiWrapper
@@ -54,6 +56,8 @@
 import com.android.launcher3.util.UserIconInfo.TYPE_WORK
 import com.android.launcher3.widget.LauncherWidgetHolder
 import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
 import java.io.StringReader
 import org.junit.After
 import org.junit.Before
@@ -162,7 +166,9 @@
     @Test
     fun work_item_added_to_home() {
         val apiWrapperMock = spy(ApiWrapper.INSTANCE[targetContext])
-        targetContext.putObject(ApiWrapper.INSTANCE, apiWrapperMock)
+        targetContext.initDaggerComponent(
+            DaggerAutoInstallsLayoutTestComponent.builder().bindApiWrapper(apiWrapperMock)
+        )
         doReturn(
                 mapOf(
                     myUserHandle() to UserIconInfo(myUserHandle(), TYPE_MAIN, 0),
@@ -198,7 +204,7 @@
             callback,
             SourceResources.wrap(targetContext.resources),
             { Xml.newPullParser().also { it.setInput(StringReader(build())) } },
-            TAG_WORKSPACE
+            TAG_WORKSPACE,
         )
 
     class MyCallback : LayoutParserCallback {
@@ -214,3 +220,14 @@
         }
     }
 }
+
+@LauncherAppSingleton
+@Component
+interface AutoInstallsLayoutTestComponent : LauncherAppComponent {
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        @BindsInstance fun bindApiWrapper(wrapper: ApiWrapper): Builder
+
+        override fun build(): AutoInstallsLayoutTestComponent
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 0538870..954dc8f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -47,14 +47,13 @@
 abstract class FakeInvariantDeviceProfileTest {
 
     protected lateinit var context: Context
-    protected var inv: InvariantDeviceProfile? = null
-    protected val info: Info = mock()
-    protected var windowBounds: WindowBounds? = null
-    protected var isMultiWindowMode: Boolean = false
-    protected var transposeLayoutWithOrientation: Boolean = false
-    protected var useTwoPanels: Boolean = false
-    protected var isGestureMode: Boolean = true
-    protected var isTransientTaskbar: Boolean = true
+    protected lateinit var inv: InvariantDeviceProfile
+    protected val info = mock<Info>()
+    protected lateinit var windowBounds: WindowBounds
+    private var transposeLayoutWithOrientation = false
+    private var useTwoPanels = false
+    private var isGestureMode = true
+    private var isTransientTaskbar = true
 
     @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
 
@@ -73,7 +72,7 @@
             info,
             windowBounds,
             SparseArray(),
-            isMultiWindowMode,
+            /*isMultiWindowMode=*/ false,
             transposeLayoutWithOrientation,
             useTwoPanels,
             isGestureMode,
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/UtilitiesTest.kt b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
index 60a4197..d0aa7a8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
@@ -17,12 +17,19 @@
 package com.android.launcher3
 
 import android.content.Context
+import android.content.ContextWrapper
+import android.graphics.Rect
+import android.graphics.RectF
 import android.view.View
 import android.view.ViewGroup
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.util.ActivityContextWrapper
-import org.junit.Assert.*
+import kotlin.random.Random
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -30,6 +37,10 @@
 @RunWith(AndroidJUnit4::class)
 class UtilitiesTest {
 
+    companion object {
+        const val SEED = 827
+    }
+
     private lateinit var mContext: Context
 
     @Before
@@ -94,4 +105,284 @@
         assertTrue(Utilities.pointInView(view, -5f, -5f, 10f)) // Inside slop
         assertFalse(Utilities.pointInView(view, 115f, 115f, 10f)) // Outside slop
     }
+
+    @Test
+    fun testNumberBounding() {
+        assertEquals(887.99f, Utilities.boundToRange(887.99f, 0f, 1000f))
+        assertEquals(2.777f, Utilities.boundToRange(887.99f, 0f, 2.777f))
+        assertEquals(900f, Utilities.boundToRange(887.99f, 900f, 1000f))
+
+        assertEquals(9383667L, Utilities.boundToRange(9383667L, -999L, 9999999L))
+        assertEquals(9383668L, Utilities.boundToRange(9383667L, 9383668L, 9999999L))
+        assertEquals(42L, Utilities.boundToRange(9383667L, -999L, 42L))
+
+        assertEquals(345, Utilities.boundToRange(345, 2, 500))
+        assertEquals(400, Utilities.boundToRange(345, 400, 500))
+        assertEquals(300, Utilities.boundToRange(345, 2, 300))
+
+        val random = Random(SEED)
+        for (i in 1..300) {
+            val value = random.nextFloat()
+            val lowerBound = random.nextFloat()
+            val higherBound = lowerBound + random.nextFloat()
+
+            assertEquals(
+                "Utilities.boundToRange doesn't match Kotlin coerceIn",
+                value.coerceIn(lowerBound, higherBound),
+                Utilities.boundToRange(value, lowerBound, higherBound)
+            )
+            assertEquals(
+                "Utilities.boundToRange doesn't match Kotlin coerceIn",
+                value.toInt().coerceIn(lowerBound.toInt(), higherBound.toInt()),
+                Utilities.boundToRange(value.toInt(), lowerBound.toInt(), higherBound.toInt())
+            )
+            assertEquals(
+                "Utilities.boundToRange doesn't match Kotlin coerceIn",
+                value.toLong().coerceIn(lowerBound.toLong(), higherBound.toLong()),
+                Utilities.boundToRange(value.toLong(), lowerBound.toLong(), higherBound.toLong())
+            )
+            assertEquals(
+                "If the lower bound is higher than lower bound, it should return the lower bound",
+                higherBound,
+                Utilities.boundToRange(value, higherBound, lowerBound)
+            )
+        }
+    }
+
+    @Test
+    fun testTranslateOverlappingView() {
+        testConcentricOverlap()
+        leftDownCornerOverlap()
+        noOverlap()
+    }
+
+    /*
+        Test Case: Rectangle Contained Within Another Rectangle
+
+           +-------------+  <-- exclusionBounds
+           |             |
+           |   +-----+   |
+           |   |     |   |  <-- targetViewBounds
+           |   |     |   |
+           |   +-----+   |
+           |             |
+           +-------------+
+    */
+    private fun testConcentricOverlap() {
+        val targetView = View(ContextWrapper(getApplicationContext()))
+        val targetViewBounds = Rect(40, 40, 60, 60)
+        val inclusionBounds = Rect(0, 0, 100, 100)
+        val exclusionBounds = Rect(30, 30, 70, 70)
+
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_RIGHT
+        )
+        assertEquals(30f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_LEFT
+        )
+        assertEquals(-30f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_DOWN
+        )
+        assertEquals(30f, targetView.translationY)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_UP
+        )
+        assertEquals(-30f, targetView.translationY)
+    }
+
+    /*
+    Test Case: Non-Overlapping Rectangles
+
+        +-----------------+      <-- targetViewBounds
+        |                 |
+        |                 |
+        +-----------------+
+
+                 +-----------+     <-- exclusionBounds
+                 |           |
+                 |           |
+                 +-----------+
+    */
+    private fun noOverlap() {
+        val targetView = View(ContextWrapper(getApplicationContext()))
+        val targetViewBounds = Rect(10, 10, 20, 20)
+
+        val inclusionBounds = Rect(0, 0, 100, 100)
+        val exclusionBounds = Rect(30, 30, 40, 40)
+
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_RIGHT
+        )
+        assertEquals(0f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_LEFT
+        )
+        assertEquals(0f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_DOWN
+        )
+        assertEquals(0f, targetView.translationY)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_UP
+        )
+        assertEquals(0f, targetView.translationY)
+    }
+
+    /*
+    Test Case: Rectangles Overlapping at Corners
+
+       +------------+         <-- exclusionBounds
+       |            |
+    +-------+       |
+    |  |    |       |          <-- targetViewBounds
+    |  +------------+
+    |       |
+    +-------+
+    */
+    private fun leftDownCornerOverlap() {
+        val targetView = View(ContextWrapper(getApplicationContext()))
+        val targetViewBounds = Rect(20, 20, 30, 30)
+
+        val inclusionBounds = Rect(0, 0, 100, 100)
+        val exclusionBounds = Rect(25, 25, 35, 35)
+
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_RIGHT
+        )
+        assertEquals(15f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_LEFT
+        )
+        assertEquals(-5f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_DOWN
+        )
+        assertEquals(15f, targetView.translationY)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_UP
+        )
+        assertEquals(-5f, targetView.translationY)
+    }
+
+    @Test
+    fun trim() {
+        val expectedString = "Hello World"
+        assertEquals(expectedString, Utilities.trim("Hello World   "))
+        // Basic trimming
+        assertEquals(expectedString, Utilities.trim("   Hello World  "))
+        assertEquals(expectedString, Utilities.trim("   Hello World"))
+
+        // Non-breaking whitespace
+        assertEquals("Hello World", Utilities.trim("\u00A0\u00A0Hello World\u00A0\u00A0"))
+
+        // Whitespace combinations
+        assertEquals(expectedString, Utilities.trim("\t \r\n Hello World \n\r"))
+        assertEquals(expectedString, Utilities.trim("\nHello World   "))
+
+        // Null input
+        assertEquals("", Utilities.trim(null))
+
+        // Empty String
+        assertEquals("", Utilities.trim(""))
+    }
+
+    @Test
+    fun getProgress() {
+        // Basic test
+        assertEquals(0.5f, Utilities.getProgress(50f, 0f, 100f), 0.001f)
+
+        // Negative values
+        assertEquals(0.5f, Utilities.getProgress(-20f, -50f, 10f), 0.001f)
+
+        // Outside of range
+        assertEquals(1.2f, Utilities.getProgress(120f, 0f, 100f), 0.001f)
+    }
+
+    @Test
+    fun scaleRectFAboutPivot() {
+        // Enlarge
+        var rectF = RectF(10f, 20f, 50f, 80f)
+        Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.5f)
+        assertEquals(RectF(0f, 5f, 60f, 95f), rectF)
+
+        // Shrink
+        rectF = RectF(10f, 20f, 50f, 80f)
+        Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 0.5f)
+        assertEquals(RectF(20f, 35f, 40f, 65f), rectF)
+
+        // No scale
+        rectF = RectF(10f, 20f, 50f, 80f)
+        Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.0f)
+        assertEquals(RectF(10f, 20f, 50f, 80f), rectF)
+    }
+
+    @Test
+    fun rotateBounds() {
+        var rect = Rect(20, 70, 60, 80)
+        Utilities.rotateBounds(rect, 100, 100, 0)
+        assertEquals(Rect(20, 70, 60, 80), rect)
+
+        rect = Rect(20, 70, 60, 80)
+        Utilities.rotateBounds(rect, 100, 100, 1)
+        assertEquals(Rect(70, 40, 80, 80), rect)
+
+        // case removed for b/28435189
+        //        rect = Rect(20, 70, 60, 80)
+        //        Utilities.rotateBounds(rect, 100, 100, 2)
+        //        assertEquals(Rect(40, 20, 80, 30), rect)
+
+        rect = Rect(20, 70, 60, 80)
+        Utilities.rotateBounds(rect, 100, 100, 3)
+        assertEquals(Rect(20, 20, 30, 60), rect)
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt
new file mode 100644
index 0000000..e03ee46
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.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.accessibility
+
+import android.content.Context
+import android.view.ViewGroup
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.util.ActivityContextWrapper
+import java.util.function.Function
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AccessibleDragListenerAdapterTest {
+
+    private lateinit var mockViewGroup: ViewGroup
+    private lateinit var mockChildOne: CellLayout
+    private lateinit var mockChildTwo: CellLayout
+    private lateinit var mDelegateFactory: Function<CellLayout, DragAndDropAccessibilityDelegate>
+    private lateinit var adapter: AccessibleDragListenerAdapter
+    private lateinit var mContext: Context
+
+    @Before
+    fun setup() {
+        mContext = ActivityContextWrapper(getApplicationContext())
+        mockViewGroup = mock(ViewGroup::class.java)
+        mockChildOne = mock(CellLayout::class.java)
+        mockChildTwo = mock(CellLayout::class.java)
+        `when`(mockViewGroup.context).thenReturn(mContext)
+        `when`(mockViewGroup.childCount).thenReturn(2)
+        `when`(mockViewGroup.getChildAt(0)).thenReturn(mockChildOne)
+        `when`(mockViewGroup.getChildAt(1)).thenReturn(mockChildTwo)
+        // Mock Delegate factory
+        mDelegateFactory =
+            mock(Function::class.java) as Function<CellLayout, DragAndDropAccessibilityDelegate>
+        `when`(mDelegateFactory.apply(any()))
+            .thenReturn(mock(DragAndDropAccessibilityDelegate::class.java))
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+    }
+
+    @Test
+    fun `onDragStart enables accessible drag for all view children`() {
+        // Create mock view children
+        val mockDragObject = mock(DragObject::class.java)
+        val mockDragOptions = mock(DragOptions::class.java)
+
+        // Action
+        adapter.onDragStart(mockDragObject, mockDragOptions)
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+        verify(mockChildTwo).setDragAndDropAccessibilityDelegate(any())
+    }
+
+    @Test
+    fun `onDragEnd removes the accessibility delegate`() {
+        // Action
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+        adapter.onDragEnd()
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+        verify(mockChildTwo).setDragAndDropAccessibilityDelegate(null)
+    }
+
+    @Test
+    fun `onChildViewAdded sets enabled as true for childview`() {
+        // Action
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+        adapter.onChildViewAdded(mockViewGroup, mockChildOne)
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+    }
+
+    @Test
+    fun `onChildViewRemoved sets enabled as false for childview`() {
+        // Action
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+        adapter.onChildViewRemoved(mockViewGroup, mockChildOne)
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt
new file mode 100644
index 0000000..1cbe1df
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.accessibility // Use the original package
+
+// Imports
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.CellLayout
+import com.android.launcher3.folder.FolderPagedView
+import com.android.launcher3.util.ActivityContextWrapper
+import kotlin.math.min
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderAccessibilityHelperTest {
+
+    // Context
+    private lateinit var mContext: Context
+    // Mocks
+    @Mock private lateinit var mockParent: FolderPagedView
+    @Mock private lateinit var mockLayout: CellLayout
+
+    private var countX = 4
+    private var countY = 3
+    private var index = 1
+
+    // System under test
+    private lateinit var folderAccessibilityHelper: FolderAccessibilityHelper
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mContext = ActivityContextWrapper(getApplicationContext())
+        `when`(mockLayout.parent).thenReturn(mockParent)
+        `when`(mockLayout.context).thenReturn(mContext)
+
+        // mStartPosition isn't recalculated after the constructor
+        // If you want to create new tests with different starting params,
+        // rebuild the folderAccessibilityHelper object
+        val countX = 4
+        val countY = 3
+        val index = 1
+        `when`(mockParent.indexOfChild(mockLayout)).thenReturn(index)
+        `when`(mockLayout.countX).thenReturn(countX)
+        `when`(mockLayout.countY).thenReturn(countY)
+
+        folderAccessibilityHelper = FolderAccessibilityHelper(mockLayout)
+    }
+
+    // Test for intersectsValidDropTarget()
+    @Test
+    fun testIntersectsValidDropTarget() {
+        // Setup
+        val id = 5
+        val allocatedContentSize = 20
+        // Make layout function public @VisibleForTesting
+        `when`(mockParent.allocatedContentSize).thenReturn(allocatedContentSize)
+
+        // Execute
+        val result = folderAccessibilityHelper.intersectsValidDropTarget(id)
+
+        // Verify
+        val expectedResult = min(id, allocatedContentSize - (index * countX * countY) - 1)
+        assertEquals(expectedResult, result)
+    }
+
+    // Test for getLocationDescriptionForIconDrop()
+    @Test
+    fun testGetLocationDescriptionForIconDrop() {
+        // Setup
+        val id = 5
+
+        // Execute
+        val result = folderAccessibilityHelper.getLocationDescriptionForIconDrop(id)
+
+        // Verify
+        val expectedResult = "Move to position ${id + (index * countX * countY) + 1}"
+        assertEquals(expectedResult, result)
+    }
+
+    // Test for getConfirmationForIconDrop()
+    @Test
+    fun testGetConfirmationForIconDrop() {
+        // Execute
+        val result =
+            folderAccessibilityHelper.getConfirmationForIconDrop(0) // Id doesn't matter here
+
+        // Verify
+        assertEquals("Item moved", result)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java b/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
index d2238ff..d938119 100644
--- a/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
@@ -65,6 +65,7 @@
     private static final int PRIVATE_SPACE_HEADER_ITEM_COUNT = 1;
     private static final int MAIN_USER_APP_COUNT = 2;
     private static final int PRIVATE_USER_APP_COUNT = 2;
+    private static final int VIEW_AT_END_OF_APP_LIST = 1;
     private static final int NUM_APP_COLS = 4;
     private static final int NUM_APP_ROWS = 3;
     private static final int PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT = 1;
@@ -107,7 +108,8 @@
                 && info.user.equals(MAIN_HANDLE));
 
         assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT
-                + PRIVATE_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size());
+                + PRIVATE_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
+                mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT,
                 mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
                         item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size());
@@ -136,7 +138,7 @@
                 && info.user.equals(MAIN_HANDLE));
 
         assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT
-                + PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT
+                + PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT + VIEW_AT_END_OF_APP_LIST
                 + PRIVATE_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT,
                 mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
@@ -166,7 +168,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT,
+        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT +
+                        VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList
                 .getAdapterItems().stream().filter(item ->
@@ -187,8 +190,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT,
-                mAlphabeticalAppsList.getAdapterItems().size());
+        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT +
+                        VIEW_AT_END_OF_APP_LIST, mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList
                 .getAdapterItems().stream().filter(item ->
                         item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size());
@@ -206,7 +209,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(MAIN_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size());
+        assertEquals(MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
+                mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
                 item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size());
         assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
@@ -222,7 +226,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(2, mAlphabeticalAppsList.getAdapterItems().size());
+        assertEquals(MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
+                mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
                         item.itemInfo != null
                                 && item.itemInfo.itemType == VIEW_TYPE_PRIVATE_SPACE_HEADER)
diff --git a/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt
new file mode 100644
index 0000000..d2103ae
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.allapps
+
+import android.content.Context
+import android.platform.test.annotations.DisableFlags
+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 com.android.launcher3.Flags
+import com.android.launcher3.util.ActivityContextWrapper
+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(AndroidJUnit4::class)
+class FloatingHeaderViewTest {
+
+    @get:Rule val mSetFlagsRule = SetFlagsRule()
+
+    private lateinit var context: Context
+    private lateinit var vut: FloatingHeaderView
+
+    @Before
+    fun setUp() {
+        context = ActivityContextWrapper(getApplicationContext())
+        // TODO(b/352161553): Inflate FloatingHeaderView or R.layout.all_apps_content with proper
+        // FloatingHeaderView#setup
+        vut = FloatingHeaderView(context)
+        vut.onFinishInflate()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_FLOATING_SEARCH_BAR, Flags.FLAG_MULTILINE_SEARCH_BAR)
+    fun onHeightUpdated_whenNotMultiline_thenZeroHeight() {
+        vut.setFloatingRowsCollapsed(true)
+        val beforeHeight = vut.maxTranslation
+        vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET)
+
+        vut.onHeightUpdated()
+
+        assertThat(vut.maxTranslation).isEqualTo(beforeHeight)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MULTILINE_SEARCH_BAR)
+    @DisableFlags(Flags.FLAG_FLOATING_SEARCH_BAR)
+    fun onHeightUpdated_whenMultiline_thenHeightIsOffset() {
+        vut.setFloatingRowsCollapsed(true)
+        vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET)
+
+        vut.onHeightUpdated()
+
+        assertThat(vut.maxTranslation).isEqualTo(HEADER_HEIGHT_OFFSET)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MULTILINE_SEARCH_BAR)
+    @EnableFlags(Flags.FLAG_FLOATING_SEARCH_BAR)
+    fun onHeightUpdated_whenFloatingRowsShownAndNotMultiline_thenAddsOnlyFloatingRow() {
+        // Collapse floating rows and expand to trigger header height calculation
+        vut.setFloatingRowsCollapsed(true)
+        vut.setFloatingRowsCollapsed(false)
+        val defaultHeight = vut.maxTranslation
+        vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET)
+
+        vut.onHeightUpdated()
+
+        assertThat(vut.maxTranslation).isEqualTo(defaultHeight)
+    }
+
+    companion object {
+        private const val HEADER_HEIGHT_OFFSET = 50
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt
new file mode 100644
index 0000000..5bc57b0
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout
+
+import org.junit.Rule
+import org.junit.Test
+
+// @RunWith(AndroidJUnit4::class) b/353965234
+class CellLayoutMethodsTest {
+
+    @JvmField @Rule var cellLayoutBuilder = UnitTestCellLayoutBuilderRule()
+
+    //@Test
+    fun pointToCellExact() {
+        val width = 1000
+        val height = 1000
+        val columns = 30
+        val rows = 30
+        val cl = cellLayoutBuilder.createCellLayout(columns, rows, false, width, height)
+
+        val res = intArrayOf(0, 0)
+        for (col in 0..<columns) {
+            for (row in 0..<rows) {
+                val x = (width / columns) * col
+                val y = (height / rows) * row
+                cl.pointToCellExact(x, y, res)
+                cl.pointToCellExact(x, y, res)
+                assertValues(col, res, row, columns, rows, width, height, x, y)
+            }
+        }
+
+        cl.pointToCellExact(-10, -10, res)
+        assertValues(0, res, 0, columns, rows, width, height, -10, -10)
+        cl.pointToCellExact(width + 10, height + 10, res)
+        assertValues(columns - 1, res, rows - 1, columns, rows, width, height, -10, -10)
+    }
+
+    private fun assertValues(
+        col: Int,
+        res: IntArray,
+        row: Int,
+        columns: Int,
+        rows: Int,
+        width: Int,
+        height: Int,
+        x: Int,
+        y: Int
+    ) {
+        assert(col == res[0] && row == res[1]) {
+            "Cell Layout with values (c= $columns, r= $rows, w= $width, h= $height) didn't mapped correctly the pixels ($x, $y) to the cells ($col, $row) with result (${res[0]}, ${res[1]})"
+        }
+    }
+}
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/celllayout/HotseatReorderUnitTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
index c32461e..a3c7f4f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
@@ -104,7 +104,7 @@
         val cl = cellLayoutBuilder.createCellLayout(board.width, board.height, false)
         // The views have to be sorted or the result can vary
         board.icons
-            .map(IconPoint::getCoord)
+            .map(IconPoint::coord)
             .sortedWith(
                 Comparator.comparing { p: Any -> (p as Point).x }
                     .thenComparing { p: Any -> (p as Point).y }
@@ -120,9 +120,7 @@
                 )
             }
         board.widgets
-            .sortedWith(
-                Comparator.comparing(WidgetRect::getCellX).thenComparing(WidgetRect::getCellY)
-            )
+            .sortedWith(Comparator.comparing(WidgetRect::cellX).thenComparing(WidgetRect::cellY))
             .forEach { widget ->
                 addViewInCellLayout(
                     cl,
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
index 8a9711d..a62258c 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -17,6 +17,9 @@
 
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
+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.assertTrue;
 
@@ -37,6 +40,8 @@
 import com.android.launcher3.celllayout.testgenerator.RandomBoardGenerator;
 import com.android.launcher3.celllayout.testgenerator.RandomMultiBoardGenerator;
 import com.android.launcher3.util.ActivityContextWrapper;
+import com.android.launcher3.util.rule.TestStabilityRule;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.views.DoubleShadowBubbleTextView;
 
 import org.junit.Rule;
@@ -68,12 +73,16 @@
     private Context mApplicationContext;
 
     @Rule
+    public TestStabilityRule mTestStabilityRule = new TestStabilityRule();
+
+    @Rule
     public UnitTestCellLayoutBuilderRule mCellLayoutBuilder = new UnitTestCellLayoutBuilderRule();
 
     /**
      * This test reads existing test cases and makes sure the CellLayout produces the same
      * output for each of them for a given input.
      */
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     public void testAllCases() throws IOException {
         List<ReorderAlgorithmUnitTestCase> testCases = getTestCases(
@@ -116,6 +125,7 @@
     /**
      * Same as above but testing the Multipage CellLayout.
      */
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     public void generateValidTests_Multi() {
         Random generator = new Random(SEED);
@@ -144,8 +154,8 @@
 
     public ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX,
             int spanY, int minSpanX, int minSpanY, boolean isMulti) {
-        CellLayout cl = mCellLayoutBuilder.createCellLayout(board.getWidth(), board.getHeight(),
-                isMulti);
+        CellLayout cl = mCellLayoutBuilder.createCellLayoutDefaultSize(board.getWidth(),
+                board.getHeight(), isMulti);
 
         // The views have to be sorted or the result can vary
         board.getIcons()
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
index b63966d..f624be1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
@@ -64,11 +64,21 @@
         dp.inv.numRows = prevNumRows
     }
 
-    fun createCellLayout(width: Int, height: Int, isMulti: Boolean): CellLayout {
+    fun createCellLayoutDefaultSize(columns: Int, rows: Int, isMulti: Boolean): CellLayout {
+        return createCellLayout(columns, rows, isMulti)
+    }
+
+    fun createCellLayout(
+        columns: Int,
+        rows: Int,
+        isMulti: Boolean,
+        width: Int = 1000,
+        height: Int = 1000
+    ): CellLayout {
         val dp = getDeviceProfile()
         // modify the device profile.
-        dp.inv.numColumns = if (isMulti) width / 2 else width
-        dp.inv.numRows = height
+        dp.inv.numColumns = if (isMulti) columns / 2 else columns
+        dp.inv.numRows = rows
         dp.cellLayoutBorderSpacePx = Point(0, 0)
         val cl =
             if (isMulti) MultipageCellLayout(getWrappedContext(applicationContext, dp))
@@ -76,8 +86,8 @@
         // I put a very large number for width and height so that all the items can fit, it doesn't
         // need to be exact, just bigger than the sum of cell border
         cl.measure(
-            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
-            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY)
+            View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+            View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
         )
         return cl
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt
new file mode 100644
index 0000000..3cbfc5a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.celllayout.board
+
+import android.graphics.Point
+import android.graphics.Rect
+
+/** Represents a widget in a CellLayoutBoard */
+data class WidgetRect(
+    val type: Char,
+    val bounds: Rect,
+) {
+    val spanX: Int = bounds.right - bounds.left + 1
+    val spanY: Int = bounds.top - bounds.bottom + 1
+    val cellY: Int = bounds.bottom
+    val cellX: Int = bounds.left
+
+    fun shouldIgnore() = type == CellType.IGNORE
+
+    fun contains(x: Int, y: Int) = bounds.contains(x, y)
+}
+
+/**
+ * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+ * letter in the alphabet, A=2, B=3, C=4 ... etc.
+ */
+data class FolderPoint(val coord: Point, val type: Char) {
+    val numberIconsInside: Int = type.code - 'A'.code + 2
+}
+
+/** Represents an icon in a CellLayoutBoard */
+data class IconPoint(val coord: Point, val type: Char = CellType.ICON)
+
+object CellType {
+    // The cells marked by this will be filled by 1x1 widgets and will be ignored when
+    // validating
+    const val IGNORE = 'x'
+
+    // The cells marked by this will be filled by app icons
+    const val ICON = 'i'
+
+    // The cells marked by FOLDER will be filled by folders with 27 app icons inside
+    const val FOLDER = 'Z'
+
+    // Empty space
+    const val EMPTY = '-'
+
+    // Widget that will be saved as "main widget" for easier retrieval
+    const val MAIN_WIDGET = 'm' // Everything else will be consider a widget
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
index e5ad888..04bfee9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
@@ -88,7 +88,7 @@
 
     public WidgetRect getWidgetOfType(char type) {
         return mWidgetsRects.stream()
-                .filter(widgetRect -> widgetRect.mType == type).findFirst().orElse(null);
+                .filter(widgetRect -> widgetRect.getType() == type).findFirst().orElse(null);
     }
 
     public WidgetRect getWidgetAt(int x, int y) {
@@ -117,8 +117,8 @@
     }
 
     private void removeWidgetFromBoard(WidgetRect widget) {
-        for (int xi = widget.mBounds.left; xi <= widget.mBounds.right; xi++) {
-            for (int yi = widget.mBounds.bottom; yi <= widget.mBounds.top; yi++) {
+        for (int xi = widget.getBounds().left; xi <= widget.getBounds().right; xi++) {
+            for (int yi = widget.getBounds().bottom; yi <= widget.getBounds().top; yi++) {
                 mWidget[xi][yi] = '-';
             }
         }
@@ -127,7 +127,7 @@
     private void removeOverlappingItems(Rect rect) {
         // Remove overlapping widgets and remove them from the board
         mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
-            if (rect.intersect(widget.mBounds)) {
+            if (rect.intersect(widget.getBounds())) {
                 removeWidgetFromBoard(widget);
                 return false;
             }
@@ -135,8 +135,8 @@
         }).collect(Collectors.toList());
         // Remove overlapping icons and remove them from the board
         mIconPoints = mIconPoints.stream().filter(iconPoint -> {
-            int x = iconPoint.coord.x;
-            int y = iconPoint.coord.y;
+            int x = iconPoint.getCoord().x;
+            int y = iconPoint.getCoord().y;
             if (rect.contains(x, y)) {
                 mWidget[x][y] = '-';
                 return false;
@@ -146,8 +146,8 @@
 
         // Remove overlapping folders and remove them from the board
         mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
-            int x = folderPoint.coord.x;
-            int y = folderPoint.coord.y;
+            int x = folderPoint.getCoord().x;
+            int y = folderPoint.getCoord().y;
             if (rect.contains(x, y)) {
                 mWidget[x][y] = '-';
                 return false;
@@ -159,7 +159,7 @@
     private void removeOverlappingItems(Point p) {
         // Remove overlapping widgets and remove them from the board
         mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
-            if (IdenticalBoardComparator.Companion.touchesPoint(widget.mBounds, p)) {
+            if (IdenticalBoardComparator.Companion.touchesPoint(widget.getBounds(), p)) {
                 removeWidgetFromBoard(widget);
                 return false;
             }
@@ -167,8 +167,8 @@
         }).collect(Collectors.toList());
         // Remove overlapping icons and remove them from the board
         mIconPoints = mIconPoints.stream().filter(iconPoint -> {
-            int x = iconPoint.coord.x;
-            int y = iconPoint.coord.y;
+            int x = iconPoint.getCoord().x;
+            int y = iconPoint.getCoord().y;
             if (p.x == x && p.y == y) {
                 mWidget[x][y] = '-';
                 return false;
@@ -178,8 +178,8 @@
 
         // Remove overlapping folders and remove them from the board
         mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
-            int x = folderPoint.coord.x;
-            int y = folderPoint.coord.y;
+            int x = folderPoint.getCoord().x;
+            int y = folderPoint.getCoord().y;
             if (p.x == x && p.y == y) {
                 mWidget[x][y] = '-';
                 return false;
@@ -226,7 +226,7 @@
 
     public void removeItem(char type) {
         mWidgetsRects.stream()
-                .filter(widgetRect -> widgetRect.mType == type)
+                .filter(widgetRect -> widgetRect.getType() == type)
                 .forEach(widgetRect -> removeOverlappingItems(
                         new Point(widgetRect.getCellX(), widgetRect.getCellY())));
     }
@@ -365,10 +365,10 @@
         board.mWidth = lines[0].length();
         board.mWidgetsRects = getRects(board.mWidget);
         board.mWidgetsRects.forEach(widgetRect -> {
-            if (widgetRect.mType == CellType.MAIN_WIDGET) {
+            if (widgetRect.getType() == CellType.MAIN_WIDGET) {
                 board.mMain = widgetRect;
             }
-            board.mWidgetsMap.put(widgetRect.mType, widgetRect);
+            board.mWidgetsMap.put(widgetRect.getType(), widgetRect);
         });
         board.mIconPoints = getIconPoints(board.mWidget);
         board.mFolderPoints = getFolderPoints(board.mWidget);
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java
deleted file mode 100644
index 49c146b..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java
+++ /dev/null
@@ -1,32 +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.celllayout.board;
-
-public class CellType {
-    // The cells marked by this will be filled by 1x1 widgets and will be ignored when
-    // validating
-    public static final char IGNORE = 'x';
-    // The cells marked by this will be filled by app icons
-    public static final char ICON = 'i';
-    // The cells marked by FOLDER will be filled by folders with 27 app icons inside
-    public static final char FOLDER = 'Z';
-    // Empty space
-    public static final char EMPTY = '-';
-    // Widget that will be saved as "main widget" for easier retrieval
-    public static final char MAIN_WIDGET = 'm';
-    // Everything else will be consider a widget
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java
deleted file mode 100644
index 39ba434..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java
+++ /dev/null
@@ -1,37 +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.celllayout.board;
-
-import android.graphics.Point;
-
-public class FolderPoint {
-    public Point coord;
-    public char mType;
-
-    public FolderPoint(Point coord, char type) {
-        this.coord = coord;
-        mType = type;
-    }
-
-    /**
-     * [A-Z]: Represents a folder and number of icons in the folder is represented by
-     * the order of letter in the alphabet, A=2, B=3, C=4 ... etc.
-     */
-    public int getNumberIconsInside() {
-        return (mType - 'A') + 2;
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java
deleted file mode 100644
index d3d2970..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.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.launcher3.celllayout.board;
-
-import android.graphics.Point;
-
-public class IconPoint {
-    public Point coord;
-    public char mType;
-
-    public IconPoint(Point coord, char type) {
-        this.coord = coord;
-        mType = type;
-    }
-
-    public char getType() {
-        return mType;
-    }
-
-    public void setType(char type) {
-        mType = type;
-    }
-
-    public Point getCoord() {
-        return coord;
-    }
-
-    public void setCoord(Point coord) {
-        this.coord = coord;
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt
index a4a420c..aacd940 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt
@@ -26,11 +26,11 @@
 
     /** Converts a list of WidgetRect into a map of the count of different widget.bounds */
     private fun widgetsToBoundsMap(widgets: List<WidgetRect>) =
-        widgets.groupingBy { it.mBounds }.eachCount()
+        widgets.groupingBy { it.bounds }.eachCount()
 
     /** Converts a list of IconPoint into a map of the count of different icon.coord */
     private fun iconsToPosCountMap(widgets: List<IconPoint>) =
-        widgets.groupingBy { it.getCoord() }.eachCount()
+        widgets.groupingBy { it.coord }.eachCount()
 
     override fun compare(
         cellLayoutBoard: CellLayoutBoard,
@@ -47,7 +47,7 @@
             widgetsToBoundsMap(
                 otherCellLayoutBoard.widgets
                     .filter { !it.shouldIgnore() }
-                    .filter { !overlapsWithIgnored(ignoredRectangles, it.mBounds) }
+                    .filter { !overlapsWithIgnored(ignoredRectangles, it.bounds) }
             )
 
         if (widgetsMap != otherWidgetMap) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java
deleted file mode 100644
index 8a427dd..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java
+++ /dev/null
@@ -1,192 +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.celllayout.board;
-
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.launcher3.ui.TestViewHelpers.findWidgetProvider;
-import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.graphics.Rect;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.Log;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.FolderInfo;
-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.widget.LauncherAppWidgetProviderInfo;
-
-import java.util.function.Supplier;
-import java.util.stream.IntStream;
-
-public class TestWorkspaceBuilder {
-
-    private static final String TAG = "CellLayoutBoardBuilder";
-    private static final String TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests.";
-    private ComponentName mAppComponentName = new ComponentName(
-            "com.google.android.calculator", "com.android.calculator2.Calculator");
-    private UserHandle mMyUser;
-
-    private Context mContext;
-
-    public TestWorkspaceBuilder(Context context) {
-        mMyUser = Process.myUserHandle();
-        mContext = context;
-    }
-
-    /**
-     * Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases.
-     */
-    private FavoriteItemsTransaction fillWithWidgets(WidgetRect widgetRect,
-            FavoriteItemsTransaction transaction, int screenId) {
-        int initX = widgetRect.getCellX();
-        int initY = widgetRect.getCellY();
-        for (int x = initX; x < initX + widgetRect.getSpanX(); x++) {
-            for (int y = initY; y < initY + widgetRect.getSpanY(); y++) {
-                try {
-                    // this widgets are filling, we don't care if we can't place them
-                    transaction.addItem(createWidgetInCell(
-                            new WidgetRect(CellType.IGNORE,
-                                    new Rect(x, y, x, y)), screenId));
-                } catch (Exception e) {
-                    Log.d(TAG, "Unable to place filling widget at " + x + "," + y);
-                }
-            }
-        }
-        return transaction;
-    }
-
-    private AppInfo getApp() {
-        return new AppInfo(mAppComponentName, "test icon", mMyUser,
-                AppInfo.makeLaunchIntent(mAppComponentName));
-    }
-
-    /**
-     * Helper to set the app to use for the test workspace,
-     *  using activity-alias from AndroidManifest-common.
-     * @param testAppName the android:name field of the test app activity-alias to use
-     */
-    public void setTestAppActivityAlias(String testAppName) {
-        this.mAppComponentName = new ComponentName(
-            getInstrumentation().getContext().getPackageName(),
-        TEST_ACTIVITY_PACKAGE_PREFIX + testAppName
-        );
-    }
-
-    private void addCorrespondingWidgetRect(WidgetRect widgetRect,
-            FavoriteItemsTransaction transaction, int screenId) {
-        if (widgetRect.mType == 'x') {
-            fillWithWidgets(widgetRect, transaction, screenId);
-        } else {
-            transaction.addItem(createWidgetInCell(widgetRect, screenId));
-        }
-    }
-
-    /**
-     * Builds the given board into the transaction
-     */
-    public FavoriteItemsTransaction buildFromBoard(CellLayoutBoard board,
-            FavoriteItemsTransaction transaction, final int screenId) {
-        board.getWidgets().forEach(
-                (widgetRect) -> addCorrespondingWidgetRect(widgetRect, transaction, screenId));
-        board.getIcons().forEach((iconPoint) ->
-                transaction.addItem(() -> createIconInCell(iconPoint, screenId))
-        );
-        board.getFolders().forEach((folderPoint) ->
-                transaction.addItem(() -> createFolderInCell(folderPoint, screenId))
-        );
-        return transaction;
-    }
-
-    /**
-     * Fills the hotseat row with apps instead of suggestions, for this to work the workspace should
-     * be clean otherwise this doesn't overrides the existing icons.
-     */
-    public FavoriteItemsTransaction fillHotseatIcons(FavoriteItemsTransaction transaction) {
-        IntStream.range(0, InvariantDeviceProfile.INSTANCE.get(mContext).numDatabaseHotseatIcons)
-                .forEach(i -> transaction.addItem(() -> getHotseatValues(i)));
-        return transaction;
-    }
-
-    private Supplier<ItemInfo> createWidgetInCell(
-            WidgetRect widgetRect, int screenId) {
-        // Create the widget lazily since the appWidgetId can get lost during setup
-        return () -> {
-            LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
-            LauncherAppWidgetInfo item = createWidgetInfo(info, getApplicationContext(), true);
-            item.cellX = widgetRect.getCellX();
-            item.cellY = widgetRect.getCellY();
-            item.spanX = widgetRect.getSpanX();
-            item.spanY = widgetRect.getSpanY();
-            item.screenId = screenId;
-            return item;
-        };
-    }
-
-    public FolderInfo createFolderInCell(FolderPoint folderPoint, int screenId) {
-        FolderInfo folderInfo = new FolderInfo();
-        folderInfo.screenId = screenId;
-        folderInfo.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-        folderInfo.cellX = folderPoint.coord.x;
-        folderInfo.cellY = folderPoint.coord.y;
-        folderInfo.minSpanY = folderInfo.minSpanX = folderInfo.spanX = folderInfo.spanY = 1;
-        folderInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null);
-
-        for (int i = 0; i < folderPoint.getNumberIconsInside(); i++) {
-            folderInfo.add(getDefaultWorkspaceItem(screenId), false);
-        }
-
-        return folderInfo;
-    }
-
-    private WorkspaceItemInfo getDefaultWorkspaceItem(int screenId) {
-        WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.screenId = screenId;
-        item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
-        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-        return item;
-    }
-
-    private ItemInfo createIconInCell(IconPoint iconPoint, int screenId) {
-        WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.screenId = screenId;
-        item.cellX = iconPoint.getCoord().x;
-        item.cellY = iconPoint.getCoord().y;
-        item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
-        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-        return item;
-    }
-
-    private ItemInfo getHotseatValues(int x) {
-        WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.cellX = x;
-        item.cellY = 0;
-        item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
-        item.rank = x;
-        item.screenId = x;
-        item.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-        return item;
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
new file mode 100644
index 0000000..8952b85
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.celllayout.board
+
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.Rect
+import android.os.Process
+import android.os.UserHandle
+import android.util.Log
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.celllayout.FavoriteItemsTransaction
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.ui.TestViewHelpers
+import com.android.launcher3.util.WidgetUtils
+import java.util.function.Supplier
+
+class TestWorkspaceBuilder(private val mContext: Context) {
+
+    private var appComponentName =
+        ComponentName("com.google.android.calculator", "com.android.calculator2.Calculator")
+    private val myUser: UserHandle = Process.myUserHandle()
+
+    /** Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases. */
+    private fun fillWithWidgets(
+        widgetRect: WidgetRect,
+        transaction: FavoriteItemsTransaction,
+        screenId: Int
+    ): FavoriteItemsTransaction {
+        val initX = widgetRect.cellX
+        val initY = widgetRect.cellY
+        for (x in initX until initX + widgetRect.spanX) {
+            for (y in initY until initY + widgetRect.spanY) {
+                try {
+                    // this widgets are filling, we don't care if we can't place them
+                    transaction.addItem(
+                        createWidgetInCell(WidgetRect(CellType.IGNORE, Rect(x, y, x, y)), screenId)
+                    )
+                } catch (e: Exception) {
+                    Log.d(TAG, "Unable to place filling widget at $x,$y")
+                }
+            }
+        }
+        return transaction
+    }
+
+    private fun app() =
+        AppInfo(appComponentName, "test icon", myUser, AppInfo.makeLaunchIntent(appComponentName))
+
+    /**
+     * Helper to set the app to use for the test workspace, using activity-alias from
+     * AndroidManifest-common.
+     *
+     * @param testAppName the android:name field of the test app activity-alias to use
+     */
+    fun setTestAppActivityAlias(testAppName: String) {
+        appComponentName =
+            ComponentName(
+                InstrumentationRegistry.getInstrumentation().context.packageName,
+                TEST_ACTIVITY_PACKAGE_PREFIX + testAppName
+            )
+    }
+
+    private fun addCorrespondingWidgetRect(
+        widgetRect: WidgetRect,
+        transaction: FavoriteItemsTransaction,
+        screenId: Int
+    ) {
+        if (widgetRect.type == 'x') {
+            fillWithWidgets(widgetRect, transaction, screenId)
+        } else {
+            transaction.addItem(createWidgetInCell(widgetRect, screenId))
+        }
+    }
+
+    /** Builds the given board into the transaction */
+    fun buildFromBoard(
+        board: CellLayoutBoard,
+        transaction: FavoriteItemsTransaction,
+        screenId: Int
+    ): FavoriteItemsTransaction {
+        board.widgets.forEach { addCorrespondingWidgetRect(it, transaction, screenId) }
+        board.icons.forEach { transaction.addItem { createIconInCell(it, screenId) } }
+        board.folders.forEach { transaction.addItem { createFolderInCell(it, screenId) } }
+        return transaction
+    }
+
+    /**
+     * Fills the hotseat row with apps instead of suggestions, for this to work the workspace should
+     * be clean otherwise this doesn't overrides the existing icons.
+     */
+    fun fillHotseatIcons(transaction: FavoriteItemsTransaction): FavoriteItemsTransaction {
+        for (i in 0..<InvariantDeviceProfile.INSTANCE[mContext].numDatabaseHotseatIcons) {
+            transaction.addItem { getHotseatValues(i) }
+        }
+        return transaction
+    }
+
+    private fun createWidgetInCell(widgetRect: WidgetRect, paramScreenId: Int): Supplier<ItemInfo> {
+        // Create the widget lazily since the appWidgetId can get lost during setup
+        return Supplier<ItemInfo> {
+            WidgetUtils.createWidgetInfo(
+                    TestViewHelpers.findWidgetProvider(false),
+                    ApplicationProvider.getApplicationContext(),
+                    true
+                )
+                .apply {
+                    cellX = widgetRect.cellX
+                    cellY = widgetRect.cellY
+                    spanX = widgetRect.spanX
+                    spanY = widgetRect.spanY
+                    screenId = paramScreenId
+                }
+        }
+    }
+
+    fun createFolderInCell(folderPoint: FolderPoint, paramScreenId: Int): FolderInfo =
+        FolderInfo().apply {
+            screenId = paramScreenId
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+            cellX = folderPoint.coord.x
+            cellY = folderPoint.coord.y
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null)
+            for (i in 0 until folderPoint.numberIconsInside) {
+                add(getDefaultWorkspaceItem(paramScreenId), false)
+            }
+        }
+
+    private fun getDefaultWorkspaceItem(paramScreenId: Int): WorkspaceItemInfo =
+        WorkspaceItemInfo(app()).apply {
+            screenId = paramScreenId
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+        }
+
+    private fun createIconInCell(iconPoint: IconPoint, paramScreenId: Int) =
+        WorkspaceItemInfo(app()).apply {
+            screenId = paramScreenId
+            cellX = iconPoint.coord.x
+            cellY = iconPoint.coord.y
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+        }
+
+    private fun getHotseatValues(x: Int) =
+        WorkspaceItemInfo(app()).apply {
+            cellX = x
+            cellY = 0
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            rank = x
+            screenId = x
+            container = LauncherSettings.Favorites.CONTAINER_HOTSEAT
+        }
+
+    companion object {
+        private const val TAG = "CellLayoutBoardBuilder"
+        private const val TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests."
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java
deleted file mode 100644
index c90ce85..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java
+++ /dev/null
@@ -1,59 +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.celllayout.board;
-
-import android.graphics.Rect;
-
-public class WidgetRect {
-    public char mType;
-    public Rect mBounds;
-
-    public WidgetRect(char type, Rect bounds) {
-        this.mType = type;
-        this.mBounds = bounds;
-    }
-
-    public int getSpanX() {
-        return mBounds.right - mBounds.left + 1;
-    }
-
-    public int getSpanY() {
-        return mBounds.top - mBounds.bottom + 1;
-    }
-
-    public int getCellX() {
-        return mBounds.left;
-    }
-
-    public int getCellY() {
-        return mBounds.bottom;
-    }
-
-    boolean shouldIgnore() {
-        return this.mType == CellType.IGNORE;
-    }
-
-    boolean contains(int x, int y) {
-        return mBounds.contains(x, y);
-    }
-
-    @Override
-    public String toString() {
-        return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY()
-                + " xs = " + getSpanX() + " ys = " + getSpanY();
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameInfosTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
new file mode 100644
index 0000000..b491f17
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.folder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.folder.FolderNameInfos.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class Label(val index: Int, val label: String, val score: Float)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderNameInfosTest {
+
+    companion object {
+        val statusList =
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+                ERROR_NO_PROVIDER,
+                ERROR_APP_LOOKUP_FAILED,
+                ERROR_ALL_APP_LOOKUP_FAILED,
+                ERROR_NO_LABELS_GENERATED,
+                ERROR_LABEL_LOOKUP_FAILED,
+                ERROR_ALL_LABEL_LOOKUP_FAILED,
+                ERROR_NO_PACKAGES,
+            )
+    }
+
+    @Test
+    fun status() {
+        assertStatus(statusList)
+        assertStatus(
+            listOf(
+                ERROR_NO_PROVIDER,
+                ERROR_APP_LOOKUP_FAILED,
+                ERROR_ALL_APP_LOOKUP_FAILED,
+                ERROR_NO_LABELS_GENERATED,
+                ERROR_LABEL_LOOKUP_FAILED,
+                ERROR_ALL_LABEL_LOOKUP_FAILED,
+                ERROR_NO_PACKAGES,
+            )
+        )
+        assertStatus(
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+            )
+        )
+        assertStatus(
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+            )
+        )
+    }
+
+    fun assertStatus(statusList: List<Int>) {
+        var infos = FolderNameInfos()
+        statusList.forEach { infos.setStatus(it) }
+        assert(infos.status() == statusList.sum()) {
+            "There is an overlap on the status constants!"
+        }
+    }
+
+    @Test
+    fun hasPrimary() {
+        assertHasPrimary(
+            createNameInfos(listOf(Label(0, "label", 1f)), statusList),
+            hasPrimary = true
+        )
+        assertHasPrimary(
+            createNameInfos(listOf(Label(1, "label", 1f)), statusList),
+            hasPrimary = false
+        )
+        assertHasPrimary(
+            createNameInfos(
+                listOf(Label(0, "label", 1f)),
+                listOf(
+                    ERROR_NO_PROVIDER,
+                    ERROR_APP_LOOKUP_FAILED,
+                    ERROR_ALL_APP_LOOKUP_FAILED,
+                    ERROR_NO_LABELS_GENERATED,
+                    ERROR_LABEL_LOOKUP_FAILED,
+                    ERROR_ALL_LABEL_LOOKUP_FAILED,
+                    ERROR_NO_PACKAGES,
+                )
+            ),
+            hasPrimary = false
+        )
+    }
+
+    private fun assertHasPrimary(nameInfos: FolderNameInfos, hasPrimary: Boolean) =
+        assert(nameInfos.hasPrimary() == hasPrimary)
+
+    private fun createNameInfos(labels: List<Label>?, statusList: List<Int>?): FolderNameInfos {
+        val infos = FolderNameInfos()
+        labels?.forEach { infos.setLabel(it.index, it.label, it.score) }
+        statusList?.forEach { infos.setStatus(it) }
+        return infos
+    }
+
+    @Test
+    fun hasSuggestions() {
+        assertHasSuggestions(
+            createNameInfos(listOf(Label(0, "label", 1f)), null),
+            hasSuggestions = true
+        )
+        assertHasSuggestions(createNameInfos(null, null), hasSuggestions = false)
+        // There is a max of 4 suggestions
+        assertHasSuggestions(
+            createNameInfos(listOf(Label(5, "label", 1f)), null),
+            hasSuggestions = false
+        )
+        assertHasSuggestions(
+            createNameInfos(
+                listOf(
+                    Label(0, "label", 1f),
+                    Label(1, "label", 1f),
+                    Label(2, "label", 1f),
+                    Label(3, "label", 1f)
+                ),
+                null
+            ),
+            hasSuggestions = true
+        )
+    }
+
+    private fun assertHasSuggestions(nameInfos: FolderNameInfos, hasSuggestions: Boolean) =
+        assert(nameInfos.hasSuggestions() == hasSuggestions)
+
+    @Test
+    fun hasContains() {
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "label3", 1f),
+                    Label(3, "label4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "label3", -1f),
+            contains = true
+        )
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "label3", 1f),
+                    Label(3, "label4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "label5", -1f),
+            contains = false
+        )
+        assertContains(
+            createNameInfos(null, null),
+            label = Label(-1, "label1", -1f),
+            contains = false
+        )
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "lAbel3", 1f),
+                    Label(3, "lEbel4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "LaBEl3", -1f),
+            contains = true
+        )
+    }
+
+    private fun assertContains(nameInfos: FolderNameInfos, label: Label, contains: Boolean) =
+        assert(nameInfos.contains(label.label) == contains)
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/FolderPagedViewTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
new file mode 100644
index 0000000..e8681dc
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.folder
+
+import android.graphics.Point
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class TestCase(val maxCountX: Int, val maxCountY: Int, val totalItems: Int)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderPagedViewTest {
+
+    companion object {
+        private fun makeFolderGridOrganizer(testCase: TestCase): FolderGridOrganizer {
+            val folderGridOrganizer = FolderGridOrganizer(testCase.maxCountX, testCase.maxCountY)
+            folderGridOrganizer.setContentSize(testCase.totalItems)
+            return folderGridOrganizer
+        }
+    }
+
+    @Test
+    fun setContentSize() {
+        assertCountXandY(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedCountX = 4,
+            expectedCountY = 3
+        )
+        assertCountXandY(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 8),
+            expectedCountX = 3,
+            expectedCountY = 3
+        )
+        assertCountXandY(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 3),
+            expectedCountX = 2,
+            expectedCountY = 2
+        )
+    }
+
+    private fun assertCountXandY(testCase: TestCase, expectedCountX: Int, expectedCountY: Int) {
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        assert(folderGridOrganizer.countX == expectedCountX) {
+            "Error on expected countX $expectedCountX got ${folderGridOrganizer.countX} using test case $testCase"
+        }
+        assert(folderGridOrganizer.countY == expectedCountY) {
+            "Error on expected countY $expectedCountY got ${folderGridOrganizer.countY} using test case $testCase"
+        }
+    }
+
+    @Test
+    fun getPosForRank() {
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(0, 0),
+            rank = 0
+        )
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(1, 0),
+            rank = 1
+        )
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(3, 0),
+            rank = 3
+        )
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(2, 1),
+            rank = 6
+        )
+        val testCase = TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22)
+        // Rank 16 and 38 should yield the same point since 38 % 12 == 2
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        assertFolderRank(testCase, expectedPos = folderGridOrganizer.getPosForRank(2), rank = 38)
+    }
+
+    private fun assertFolderRank(testCase: TestCase, expectedPos: Point, rank: Int) {
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        val pos = folderGridOrganizer.getPosForRank(rank)
+        assert(pos == expectedPos) {
+            "Expected pos = $expectedPos  doesn't match pos = $pos for the given rank $rank and the give test case $testCase"
+        }
+    }
+
+    @Test
+    fun isItemInPreview() {
+        val folderGridOrganizer = FolderGridOrganizer(5, 8)
+        folderGridOrganizer.setContentSize(ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1)
+        // Very few items
+        for (i in 0..3) {
+            assertItemsInPreview(
+                TestCase(
+                    maxCountX = 5,
+                    maxCountY = 8,
+                    totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+                ),
+                expectedIsInPreview = true,
+                page = 0,
+                rank = i
+            )
+        }
+        for (i in 4..40) {
+            assertItemsInPreview(
+                TestCase(
+                    maxCountX = 5,
+                    maxCountY = 8,
+                    totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+                ),
+                expectedIsInPreview = false,
+                page = 0,
+                rank = i
+            )
+        }
+        // Full of items
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = false,
+            page = 0,
+            rank = 2
+        )
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = false,
+            page = 0,
+            rank = 2
+        )
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = true,
+            page = 0,
+            rank = 5
+        )
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = true,
+            page = 0,
+            rank = 6
+        )
+    }
+
+    private fun assertItemsInPreview(
+        testCase: TestCase,
+        expectedIsInPreview: Boolean,
+        page: Int,
+        rank: Int
+    ) {
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        val isInPreview = folderGridOrganizer.isItemInPreview(page, rank)
+        assert(isInPreview == expectedIsInPreview) {
+            "Item preview state should be $expectedIsInPreview but got $isInPreview, for page $page and rank $rank, for test case $testCase"
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/FolderTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/FolderTest.kt
new file mode 100644
index 0000000..4eb335e
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/FolderTest.kt
@@ -0,0 +1,928 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.folder
+
+import android.content.Context
+import android.graphics.Point
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Alarm
+import com.android.launcher3.DragSource
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.LauncherAppState
+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_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.OnAlarmListener
+import com.android.launcher3.R
+import com.android.launcher3.celllayout.board.FolderPoint
+import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.dragndrop.DragView
+import com.android.launcher3.folder.Folder.MIN_CONTENT_DIMEN
+import com.android.launcher3.folder.Folder.ON_EXIT_CLOSE_DELAY
+import com.android.launcher3.folder.Folder.SCROLL_LEFT
+import com.android.launcher3.folder.Folder.SCROLL_NONE
+import com.android.launcher3.folder.Folder.STATE_ANIMATING
+import com.android.launcher3.folder.Folder.STATE_CLOSED
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import java.util.ArrayList
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+
+/** Tests for [Folder] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderTest {
+
+    private val context: Context =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+    private val workspaceBuilder = TestWorkspaceBuilder(context)
+    private val folder: Folder = spy(Folder(context, null))
+
+    @After
+    fun tearDown() {
+        LauncherAppState.getInstance(context).model.clearModelDb()
+    }
+
+    @Test
+    fun `Undo a folder with 1 icon when onDropCompleted is called`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+        folder.mInfo.getContents().removeAt(0)
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val dragLayout = Mockito.mock(View::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.deleteFolderOnDropCompleted = false
+
+        folder.onDropCompleted(dragLayout, dragObject, true)
+
+        verify(folder, times(1)).replaceFolderWithFinalItem()
+        assertEquals(folder.deleteFolderOnDropCompleted, false)
+    }
+
+    @Test
+    fun `Do not undo a folder with 2 icons when onDropCompleted is called`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val dragLayout = Mockito.mock(View::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.deleteFolderOnDropCompleted = false
+
+        folder.onDropCompleted(dragLayout, dragObject, true)
+
+        verify(folder, times(0)).replaceFolderWithFinalItem()
+        assertEquals(folder.deleteFolderOnDropCompleted, false)
+    }
+
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_APPLICATION`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPLICATION
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_DEEP_SHORTCUT`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_APP_PAIR`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APP_PAIR
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we do not accept invalid item type ITEM_TYPE_APPWIDGET`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPWIDGET
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertFalse(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we do not accept invalid item type ITEM_TYPE_FOLDER`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_FOLDER
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertFalse(willAcceptResult)
+    }
+
+    @Test
+    fun `We should not animate open if items is null or less than or equal to 1`() {
+        folder.mInfo = Mockito.mock(FolderInfo::class.java)
+        val shouldAnimateOpenResult = folder.shouldAnimateOpen(null)
+
+        assertFalse(shouldAnimateOpenResult)
+        assertFalse(
+            folder.shouldAnimateOpen(arrayListOf<ItemInfo>(Mockito.mock(ItemInfo::class.java)))
+        )
+    }
+
+    @Test
+    fun `We should animate open if items greater than 1`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val shouldAnimateOpenResult = folder.shouldAnimateOpen(folder.mInfo.getContents())
+
+        assertTrue(shouldAnimateOpenResult)
+    }
+
+    @Test
+    fun `Should be true if there is an open folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(Mockito.mock(Folder::class.java))
+
+        assertTrue(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `Should be false if the open folder is this folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(folder)
+
+        assertFalse(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `Should be false if there is not an open folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(null)
+
+        assertFalse(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `If drag is in progress we should set mItemAddedBackToSelfViaIcon to true`() {
+        folder.itemAddedBackToSelfViaIcon = false
+        folder.isDragInProgress = true
+
+        folder.notifyDrop()
+
+        assertTrue(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `If drag is not in progress we should not set mItemAddedBackToSelfViaIcon to true`() {
+        folder.itemAddedBackToSelfViaIcon = false
+        folder.isDragInProgress = false
+
+        folder.notifyDrop()
+
+        assertFalse(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `If launcher dragging is not enabled onLongClick should return true`() {
+        `when`(folder.isLauncherDraggingEnabled).thenReturn(false)
+
+        val onLongClickResult = folder.onLongClick(Mockito.mock(View::class.java))
+
+        assertTrue(onLongClickResult)
+    }
+
+    @Test
+    fun `If launcher dragging is enabled we should return startDrag result`() {
+        `when`(folder.isLauncherDraggingEnabled).thenReturn(true)
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = Mockito.mock(DragOptions::class.java)
+
+        val onLongClickResult = folder.onLongClick(viewMock)
+
+        assertEquals(onLongClickResult, folder.startDrag(viewMock, dragOptions))
+        verify(folder, times(1)).startDrag(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify start drag works as intended when view is instanceof ItemInfo`() {
+        val itemInfo = ItemInfo()
+        itemInfo.rank = 5
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = DragOptions()
+        `when`(viewMock.tag).thenReturn(itemInfo)
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.startDrag(viewMock, dragOptions)
+
+        assertEquals(folder.mEmptyCellRank, 5)
+        assertEquals(folder.currentDragView, viewMock)
+        verify(folder, times(1)).addDragListener(dragOptions)
+        verify(folder, times(1)).callBeginDragShared(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify start drag works as intended when view is not instanceof ItemInfo`() {
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = DragOptions()
+
+        folder.startDrag(viewMock, dragOptions)
+
+        verify(folder, times(0)).addDragListener(dragOptions)
+        verify(folder, times(0)).callBeginDragShared(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify that onDragStart has an effect if dragSource is this folder`() {
+        folder.itemsInvalidated = false
+        folder.isDragInProgress = false
+        folder.itemAddedBackToSelfViaIcon = true
+        folder.currentDragView = Mockito.mock(View::class.java)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        val dragObject = DragObject(context)
+        dragObject.dragInfo = Mockito.mock(ItemInfo::class.java)
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        dragObject.dragSource = folder
+
+        folder.onDragStart(dragObject, DragOptions())
+
+        verify(folder.mContent, times(1)).removeItem(folder.currentDragView)
+        verify(folder.mInfo, times(1)).remove(dragObject.dragInfo, true)
+        assertTrue(folder.itemsInvalidated)
+        assertTrue(folder.isDragInProgress)
+        assertFalse(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `Verify that onDragStart has no effects if dragSource is not this folder`() {
+        folder.itemsInvalidated = false
+        folder.isDragInProgress = false
+        folder.itemAddedBackToSelfViaIcon = true
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val dragObject = DragObject(context)
+        dragObject.dragSource = Mockito.mock(DragSource::class.java)
+
+        folder.onDragStart(dragObject, DragOptions())
+
+        verify(folder.mContent, times(0)).removeItem(folder.currentDragView)
+        assertFalse(folder.itemsInvalidated)
+        assertFalse(folder.isDragInProgress)
+        assertTrue(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `Verify onDragEnd that we call completeDragExit and set drag in progress false`() {
+        doNothing().`when`(folder).completeDragExit()
+        folder.isExternalDrag = true
+        folder.isDragInProgress = true
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.onDragEnd()
+
+        verify(folder, times(1)).completeDragExit()
+        verify(folder.dragController, times(1)).removeDragListener(folder)
+        assertFalse(folder.isDragInProgress)
+    }
+
+    @Test
+    fun `Verify onDragEnd that we do not call completeDragExit and set drag in progress false`() {
+        folder.isExternalDrag = false
+        folder.isDragInProgress = true
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.onDragEnd()
+
+        verify(folder, times(0)).completeDragExit()
+        verify(folder.dragController, times(1)).removeDragListener(folder)
+        assertFalse(folder.isDragInProgress)
+    }
+
+    @Test
+    fun `startEditingFolderName should set hint to empty and showLabelSuggestions`() {
+        doNothing().`when`(folder).showLabelSuggestions()
+        folder.isEditingName = false
+        folder.folderName = FolderNameEditText(context)
+        folder.folderName.hint = "hello"
+
+        folder.startEditingFolderName()
+
+        verify(folder, times(1)).showLabelSuggestions()
+        assertEquals("", folder.folderName.hint)
+        assertTrue(folder.isEditingName)
+    }
+
+    @Test
+    fun `Ensure we set the title and hint correctly onBackKey when we have a new title`() {
+        val expectedHint = null
+        val expectedTitle = "hello"
+        folder.isEditingName = true
+        folder.folderName = spy(FolderNameEditText(context))
+        folder.folderName.setText(expectedTitle)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        folder.mInfo.title = "world"
+        folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+        folder.onBackKey()
+
+        assertEquals(expectedTitle, folder.mInfo.title)
+        verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+        assertEquals(expectedHint, folder.folderName.hint)
+        assertFalse(folder.isEditingName)
+        verify(folder.folderName, times(1)).clearFocus()
+    }
+
+    @Test
+    fun `Ensure we set the title and hint correctly onBackKey when we do not have a new title`() {
+        val expectedHint = context.getString(R.string.folder_hint_text)
+        val expectedTitle = ""
+        folder.isEditingName = true
+        folder.folderName = spy(FolderNameEditText(context))
+        folder.folderName.setText(expectedTitle)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        folder.mInfo.title = "world"
+        folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+        folder.onBackKey()
+
+        assertEquals(expectedTitle, folder.mInfo.title)
+        verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+        assertEquals(expectedHint, folder.folderName.hint)
+        assertFalse(folder.isEditingName)
+        verify(folder.folderName, times(1)).clearFocus()
+    }
+
+    @Test
+    fun `ensure onEditorAction calls dispatchBackKey when actionId is IME_ACTION_DONE`() {
+        folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+        val result =
+            folder.onEditorAction(
+                Mockito.mock(TextView::class.java),
+                EditorInfo.IME_ACTION_DONE,
+                Mockito.mock(KeyEvent::class.java)
+            )
+
+        assertTrue(result)
+        verify(folder.folderName, times(1)).dispatchBackKey()
+    }
+
+    @Test
+    fun `ensure onEditorAction does not call dispatchBackKey when actionId is not IME_ACTION_DONE`() {
+        folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+        val result =
+            folder.onEditorAction(
+                Mockito.mock(TextView::class.java),
+                EditorInfo.IME_ACTION_NONE,
+                Mockito.mock(KeyEvent::class.java)
+            )
+
+        assertFalse(result)
+        verify(folder.folderName, times(0)).dispatchBackKey()
+    }
+
+    @Test
+    fun `in completeDragExit we close the folder when mIsOpen`() {
+        doNothing().`when`(folder).close(true)
+        folder.setIsOpen(true)
+        folder.rearrangeOnClose = false
+
+        folder.completeDragExit()
+
+        verify(folder, times(1)).close(true)
+        assertTrue(folder.rearrangeOnClose)
+    }
+
+    @Test
+    fun `in completeDragExit we want to rearrange on close when it is animating`() {
+        folder.setIsOpen(false)
+        folder.rearrangeOnClose = false
+        folder.state = STATE_ANIMATING
+
+        folder.completeDragExit()
+
+        verify(folder, times(0)).close(true)
+        assertTrue(folder.rearrangeOnClose)
+    }
+
+    @Test
+    fun `in completeDragExit we want to call rearrangeChildren and clearDragInfo when not open and not animating`() {
+        doNothing().`when`(folder).rearrangeChildren()
+        doNothing().`when`(folder).clearDragInfo()
+        folder.setIsOpen(false)
+        folder.rearrangeOnClose = false
+        folder.state = STATE_CLOSED
+
+        folder.completeDragExit()
+
+        verify(folder, times(0)).close(true)
+        assertFalse(folder.rearrangeOnClose)
+        verify(folder, times(1)).rearrangeChildren()
+        verify(folder, times(1)).clearDragInfo()
+    }
+
+    @Test
+    fun `clearDragInfo should set current drag view to null and isExternalDrag to false`() {
+        folder.currentDragView = Mockito.mock(DragView::class.java)
+        folder.isExternalDrag = true
+
+        folder.clearDragInfo()
+
+        assertNull(folder.currentDragView)
+        assertFalse(folder.isExternalDrag)
+    }
+
+    @Test
+    fun `onDragExit should set alarm if drag is not complete`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        dragObject.dragComplete = false
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.onExitAlarm, times(1)).setOnAlarmListener(folder.mOnExitAlarmListener)
+        verify(folder.onExitAlarm, times(1)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+    }
+
+    @Test
+    fun `onDragExit should not set alarm if drag is complete`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        dragObject.dragComplete = true
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.onExitAlarm, times(0)).setOnAlarmListener(folder.mOnExitAlarmListener)
+        verify(folder.onExitAlarm, times(0)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+    }
+
+    @Test
+    fun `onDragExit should not clear scroll hint if already SCROLL_NONE`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_NONE
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.mContent, times(0)).clearScrollHint()
+    }
+
+    @Test
+    fun `onDragExit should clear scroll hint if not SCROLL_NONE and then set scroll hint to scroll none`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_LEFT
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.mContent, times(1)).clearScrollHint()
+        assertEquals(folder.scrollHintDir, SCROLL_NONE)
+    }
+
+    @Test
+    fun `onDragExit we should cancel reorder pause and hint alarms`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_NONE
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.onScrollHintAlarm = Mockito.mock(Alarm::class.java)
+        folder.scrollPauseAlarm = Mockito.mock(Alarm::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.reorderAlarm, times(1)).cancelAlarm()
+        verify(folder.onScrollHintAlarm, times(1)).cancelAlarm()
+        verify(folder.scrollPauseAlarm, times(1)).cancelAlarm()
+        assertEquals(folder.scrollHintDir, SCROLL_NONE)
+    }
+
+    @Test
+    fun `when calling prepareAccessibilityDrop we should cancel pending reorder alarm and call onAlarm`() {
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+        `when`(folder.reorderAlarm.alarmPending()).thenReturn(true)
+
+        folder.prepareAccessibilityDrop()
+
+        verify(folder.reorderAlarm, times(1)).cancelAlarm()
+        verify(folder.mReorderAlarmListener, times(1)).onAlarm(folder.reorderAlarm)
+    }
+
+    @Test
+    fun `when calling prepareAccessibilityDrop we should not do anything if there is no pending alarm`() {
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+        `when`(folder.reorderAlarm.alarmPending()).thenReturn(false)
+
+        folder.prepareAccessibilityDrop()
+
+        verify(folder.reorderAlarm, times(0)).cancelAlarm()
+        verify(folder.mReorderAlarmListener, times(0)).onAlarm(folder.reorderAlarm)
+    }
+
+    @Test
+    fun `isDropEnabled should be true as long as state is not STATE_ANIMATING`() {
+        folder.state = STATE_CLOSED
+
+        val isDropEnabled = folder.isDropEnabled
+
+        assertTrue(isDropEnabled)
+    }
+
+    @Test
+    fun `isDropEnabled should be false if state is STATE_ANIMATING`() {
+        folder.state = STATE_ANIMATING
+
+        val isDropEnabled = folder.isDropEnabled
+
+        assertFalse(isDropEnabled)
+    }
+
+    @Test
+    fun `getItemCount should return the number of items in the folder`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val itemCount = folder.itemCount
+
+        assertEquals(itemCount, 2)
+    }
+
+    @Test
+    fun `hideItem should set the visibility of the corresponding ItemInfo to invisible`() {
+        val itemInfo = ItemInfo()
+        val view = View(context)
+        view.isVisible = true
+        doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+        folder.hideItem(itemInfo)
+
+        assertFalse(view.isVisible)
+    }
+
+    @Test
+    fun `showItem should set the visibility of the corresponding ItemInfo to visible`() {
+        val itemInfo = ItemInfo()
+        val view = View(context)
+        view.isVisible = false
+        doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+        folder.showItem(itemInfo)
+
+        assertTrue(view.isVisible)
+    }
+
+    @Test
+    fun `onDragEnter should cancel exit alarm and set the scroll area offset to dragRegionWidth divided by two minus xOffset`() {
+        folder.mPrevTargetRank = 1
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val dragView = Mockito.mock(DragView::class.java)
+        dragObject.dragView = dragView
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        `when`(dragObject.dragView.getDragRegionWidth()).thenReturn(100)
+        dragObject.xOffset = 20
+
+        folder.onDragEnter(dragObject)
+
+        verify(folder.onExitAlarm, times(1)).cancelAlarm()
+        assertEquals(-1, folder.mPrevTargetRank)
+        assertEquals(30, folder.scrollAreaOffset)
+    }
+
+    @Test
+    fun `acceptDrop should return true with the correct item type as a parameter`() {
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APP_PAIR
+        dragObject.dragInfo = itemInfo
+
+        val result = folder.acceptDrop(dragObject)
+
+        assertTrue(result)
+    }
+
+    @Test
+    fun `acceptDrop should return false with the incorrect item type as a parameter`() {
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPWIDGET
+        dragObject.dragInfo = itemInfo
+
+        val result = folder.acceptDrop(dragObject)
+
+        assertFalse(result)
+    }
+
+    @Test
+    fun `rearrangeChildren should return early if content view are not bound`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.itemsInvalidated = false
+        doReturn(false).whenever(folder.mContent).areViewsBound()
+
+        folder.rearrangeChildren()
+
+        verify(folder.mContent, times(0)).arrangeChildren(folder.iconsInReadingOrder)
+        assertFalse(folder.itemsInvalidated)
+    }
+
+    @Test
+    fun `rearrangeChildren should call arrange children and invalidate items`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.itemsInvalidated = false
+        doReturn(true).whenever(folder.mContent).areViewsBound()
+        val iconsInReadingOrderList = ArrayList<View>()
+        `when`(folder.iconsInReadingOrder).thenReturn(iconsInReadingOrderList)
+        doNothing().`when`(folder.mContent).arrangeChildren(iconsInReadingOrderList)
+
+        folder.rearrangeChildren()
+
+        verify(folder.mContent, times(1)).arrangeChildren(folder.iconsInReadingOrder)
+        assertTrue(folder.itemsInvalidated)
+    }
+
+    @Test
+    fun `getItemCount should return the size of info getContents size`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val itemCount = folder.itemCount
+
+        assertEquals(2, itemCount)
+    }
+
+    @Test
+    fun `replaceFolderWithFinalItem should set mDestroyed to true if we replace folder with final item`() {
+        val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+        folder.mLauncherDelegate = launcherDelegate
+        `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(true)
+
+        folder.replaceFolderWithFinalItem()
+
+        assertTrue(folder.isDestroyed)
+    }
+
+    @Test
+    fun `replaceFolderWithFinalItem should set mDestroyed to false if we do not replace folder with final item`() {
+        val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+        folder.mLauncherDelegate = launcherDelegate
+        `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(false)
+
+        folder.replaceFolderWithFinalItem()
+
+        assertFalse(folder.isDestroyed)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return maxContentAreaHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(100)
+        `when`(folder.maxContentAreaHeight).thenReturn(50)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(50, height)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return desiredHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(50)
+        `when`(folder.maxContentAreaHeight).thenReturn(100)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(50, height)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return MIN_CONTENT_DIMEN`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(1)
+        `when`(folder.maxContentAreaHeight).thenReturn(2)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(MIN_CONTENT_DIMEN, height)
+    }
+
+    @Test
+    fun `getContentAreaWidth should return desired width`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(50)
+
+        val width = folder.contentAreaWidth
+
+        assertEquals(50, width)
+    }
+
+    @Test
+    fun `getContentAreaWidth should return MIN_CONTENT_DIMEN`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(1)
+
+        val width = folder.contentAreaWidth
+
+        assertEquals(MIN_CONTENT_DIMEN, width)
+    }
+
+    @Test
+    fun `getFolderWidth should return padding left plus padding right plus desired width`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(1)
+        `when`(folder.paddingLeft).thenReturn(10)
+        `when`(folder.paddingRight).thenReturn(10)
+
+        val width = folder.folderWidth
+
+        assertEquals(21, width)
+    }
+
+    @Test
+    fun `getFolderHeight with no params should return getFolderHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.contentAreaHeight).thenReturn(100)
+        `when`(folder.getFolderHeight(folder.contentAreaHeight)).thenReturn(120)
+
+        val height = folder.folderHeight
+
+        assertEquals(120, height)
+    }
+
+    @Test
+    fun `getFolderWidth with contentAreaHeight should return padding top plus padding bottom plus contentAreaHeight plus footer height`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.footerHeight).thenReturn(100)
+        `when`(folder.paddingTop).thenReturn(10)
+        `when`(folder.paddingBottom).thenReturn(10)
+
+        val height = folder.getFolderHeight(100)
+
+        assertEquals(220, height)
+    }
+
+    @Test
+    fun `onRemove should call removeItem with the correct views`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        verify(folder.mContent, times(1)).removeItem(view1)
+        verify(folder.mContent, times(1)).removeItem(view2)
+    }
+
+    @Test
+    fun `onRemove should set mRearrangeOnClose to true and not call rearrangeChildren if animating`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.state = STATE_ANIMATING
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        assertTrue(folder.rearrangeOnClose)
+        verify(folder, times(0)).rearrangeChildren()
+    }
+
+    @Test
+    fun `onRemove should set not change mRearrangeOnClose and not call rearrangeChildren if not animating`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.state = STATE_CLOSED
+        folder.rearrangeOnClose = false
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        assertFalse(folder.rearrangeOnClose)
+        verify(folder, times(1)).rearrangeChildren()
+    }
+
+    @Test
+    fun `onRemove should call close if mIsOpen is true and item count is less than or equal to one`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(1).whenever(folder).itemCount
+        folder.setIsOpen(true)
+        doNothing().`when`(folder).close(true)
+
+        folder.onRemove(items)
+
+        verify(folder, times(1)).close(true)
+    }
+
+    @Test
+    fun `onRemove should call replaceFolderWithFinalItem if mIsOpen is false and item count is less than or equal to one`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(1).whenever(folder).itemCount
+        folder.setIsOpen(false)
+
+        folder.onRemove(items)
+
+        verify(folder, times(1)).replaceFolderWithFinalItem()
+    }
+
+    companion object {
+        const val TWO_ICON_FOLDER_TYPE = 'A'
+    }
+}
diff --git a/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
similarity index 98%
rename from tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
rename to tests/multivalentTests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
index 7242e9c..b9b7d6a 100644
--- a/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
@@ -24,15 +24,15 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.PathInterpolator;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.launcher3.CellLayout;
+import com.android.launcher3.util.LauncherMultivalentJUnit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -41,8 +41,8 @@
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@UiThreadTest
+@RunWith(LauncherMultivalentJUnit.class)
 public class PreviewBackgroundTest {
 
     private static final float REST_SCALE = 1f;
diff --git a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
similarity index 74%
rename from tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index da14425..111ffaa 100644
--- a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -23,18 +23,25 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
 import com.android.launcher3.LauncherPrefs.Companion.get
+import com.android.launcher3.graphics.PreloadIconDrawable
 import com.android.launcher3.icons.BaseIconFactory
 import com.android.launcher3.icons.FastBitmapDrawable
 import com.android.launcher3.icons.UserBadgeDrawable
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED
+import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE
+import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.Executors
 import com.android.launcher3.util.FlagOp
 import com.android.launcher3.util.LauncherLayoutBuilder
 import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
 import com.android.launcher3.util.UserIconInfo
+import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -51,6 +58,8 @@
     private lateinit var modelHelper: LauncherModelHelper
     private lateinit var folderIcon: FolderIcon
 
+    private var defaultThemedIcons = false
+
     @Before
     fun setup() {
         getInstrumentation().runOnMainSync {
@@ -86,8 +95,8 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
 
         // Set second icon to be non-themed.
@@ -98,8 +107,8 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
 
         // Set third icon to be themed with badge.
@@ -110,8 +119,8 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
         folderApps[2].bitmap = folderApps[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
 
@@ -124,19 +133,23 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
+
+        defaultThemedIcons = get(context).get(THEMED_ICONS)
     }
+
     @After
     @Throws(Exception::class)
     fun tearDown() {
+        get(context).put(THEMED_ICONS, defaultThemedIcons)
         modelHelper.destroy()
     }
 
     @Test
     fun checkThemedIconWithThemingOn_iconShouldBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -146,7 +159,7 @@
 
     @Test
     fun checkThemedIconWithThemingOff_iconShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        get(context).put(THEMED_ICONS, false)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -156,7 +169,7 @@
 
     @Test
     fun checkUnthemedIconWithThemingOn_iconShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -166,7 +179,7 @@
 
     @Test
     fun checkUnthemedIconWithThemingOff_iconShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        get(context).put(THEMED_ICONS, false)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -176,7 +189,7 @@
 
     @Test
     fun checkThemedIconWithBadgeWithThemingOn_iconAndBadgeShouldBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[2])
@@ -189,7 +202,7 @@
 
     @Test
     fun checkUnthemedIconWithBadgeWithThemingOn_badgeShouldBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
@@ -202,7 +215,7 @@
 
     @Test
     fun checkUnthemedIconWithBadgeWithThemingOff_iconAndBadgeShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        get(context).put(THEMED_ICONS, false)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
@@ -213,6 +226,39 @@
         )
     }
 
+    @Test
+    fun `Inactive archived app previews are not drawn as preload icon`() {
+        // Given
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+        val archivedApp =
+            WorkspaceItemInfo().apply {
+                runtimeStatusFlags = runtimeStatusFlags or FLAG_ARCHIVED
+                runtimeStatusFlags = runtimeStatusFlags and FLAG_INSTALL_SESSION_ACTIVE.inv()
+            }
+        // When
+        previewItemManager.setDrawable(drawingParams, archivedApp)
+        // Then
+        assertThat(drawingParams.drawable).isNotInstanceOf(PreloadIconDrawable::class.java)
+    }
+
+    @Test
+    fun `Actively installing archived app previews are drawn as preload icon`() {
+        // Given
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+        val archivedApp =
+            WorkspaceItemInfo().apply {
+                runtimeStatusFlags = runtimeStatusFlags or FLAG_ARCHIVED
+                runtimeStatusFlags = runtimeStatusFlags or FLAG_INSTALL_SESSION_ACTIVE
+            }
+        // When
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {
+            // Run on main thread because preload drawable triggers animator
+            previewItemManager.setDrawable(drawingParams, archivedApp)
+        }
+        // Then
+        assertThat(drawingParams.drawable).isInstanceOf(PreloadIconDrawable::class.java)
+    }
+
     private fun profileFlagOp(type: Int) =
         UserIconInfo(Process.myUserHandle(), type).applyBitmapInfoFlags(FlagOp.NO_OP)
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
index 495d583..ce00b28 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
@@ -15,21 +15,40 @@
  */
 package com.android.launcher3.icons;
 
+import static android.os.Process.myUserHandle;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
+import static com.android.launcher3.icons.IconCacheUpdateHandlerTestKt.waitForUpdateHandlerToFinish;
+import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutInfo.Builder;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.drawable.Icon;
 import android.os.PersistableBundle;
+import android.os.UserHandle;
 import android.text.TextUtils;
 
 import androidx.annotation.Nullable;
@@ -37,15 +56,29 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
+import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.settings.SettingsActivity;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ApplicationInfoWrapper;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+
+import com.google.common.truth.Truth;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class IconCacheTest {
@@ -112,6 +145,162 @@
         assertEquals(((PackageItemInfo) item).packageName, otherPackage);
     }
 
+    @Test
+    public void launcherActivityInfo_cached_in_memory() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+        ComponentKey cacheKey = new ComponentKey(cn, user);
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        WorkspaceItemInfo info = new WorkspaceItemInfo();
+        info.intent = makeLaunchIntent(cn);
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> mIconCache.getTitleAndIcon(info, lai, false));
+        assertNotNull(info.bitmap);
+        assertFalse(info.bitmap.isLowRes());
+
+        // Verify that icon is in memory cache
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+
+        // Schedule async update and wait for it to complete
+        Set<PackageUserKey> updates =
+                executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE);
+
+        // Verify that the icon was not updated and is still in memory cache
+        Truth.assertThat(updates).isEmpty();
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+    }
+
+    @Test
+    public void shortcutInfo_not_cached_in_memory() {
+        CacheableShortcutInfo si = mockShortcutInfo(0);
+        ShortcutKey cacheKey = ShortcutKey.fromInfo(si.getShortcutInfo());
+
+        WorkspaceItemInfo info = new WorkspaceItemInfo();
+        runOnExecutorSync(MODEL_EXECUTOR, () -> mIconCache.getShortcutIcon(info, si));
+        assertNotNull(info.bitmap);
+        assertFalse(info.bitmap.isLowRes());
+
+        // Verify that icon is in memory cache
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+
+        Set<PackageUserKey> updates =
+                executeIconUpdate(si, CacheableShortcutCachingLogic.INSTANCE);
+        // Verify that the icon was not updated and is still in memory cache
+        Truth.assertThat(updates).isEmpty();
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+
+        // Now update the shortcut with a newer version
+        updates = executeIconUpdate(
+                mockShortcutInfo(System.currentTimeMillis() + 2000),
+                CacheableShortcutCachingLogic.INSTANCE);
+
+        // Verify that icon was updated but it is still not in mem-cache
+        Truth.assertThat(updates).containsExactly(
+                new PackageUserKey(cacheKey.getPackageName(), cacheKey.user));
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+    }
+
+    @Test
+    public void item_kept_in_db_if_nothing_changes() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        // Since this is a new update, there should not be any update
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+
+        // Another update should not cause any changes
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+    }
+
+    @Test
+    public void item_updated_in_db_if_appInfo_changes() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        // Since this is a new update, there should not be any update
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+
+        // Another update should trigger an update
+        lai.getApplicationInfo().sourceDir = "some-random-source-dir";
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE))
+                .containsExactly(new PackageUserKey(TEST_PACKAGE, user));
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+    }
+
+    @Test
+    public void item_removed_in_db_if_item_removed() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        // Since this is a new update, there should not be any update
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+
+        // Another update should trigger an update
+        ComponentName cn2 = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY2);
+        LauncherActivityInfo lai2 = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn2), user);
+
+        Truth.assertThat(executeIconUpdate(lai2, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertFalse(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn2, user)));
+    }
+
+    /**
+     * Executes the icon update for the provided entry and returns the updated packages
+     */
+    private <T> Set<PackageUserKey> executeIconUpdate(T object, CachingLogic<T> cachingLogic) {
+        HashSet<PackageUserKey> updates = new HashSet<>();
+
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
+            updateHandler.updateIcons(
+                    Collections.singletonList(object),
+                    cachingLogic,
+                    (a, b) -> a.forEach(p -> updates.add(new PackageUserKey(p, b))));
+            updateHandler.finish();
+        });
+        waitForUpdateHandlerToFinish(mIconCache);
+        return updates;
+    }
+
+    private CacheableShortcutInfo mockShortcutInfo(long updateTime) {
+        ShortcutInfo info = new ShortcutInfo.Builder(
+                        getInstrumentation().getContext(), "test-shortcut")
+                .setIntent(new Intent(Intent.ACTION_VIEW))
+                .setShortLabel("Test")
+                .setIcon(Icon.createWithBitmap(Bitmap.createBitmap(200, 200, Config.ARGB_8888)))
+                .build();
+        ShortcutInfo spied = spy(info);
+        doReturn(updateTime).when(spied).getLastChangedTimestamp();
+        return new CacheableShortcutInfo(spied,
+                new ApplicationInfoWrapper(getInstrumentation().getContext().getApplicationInfo()));
+    }
+
     private ItemInfoWithIcon getBadgingInfo(Context context,
             @Nullable ComponentName cn, @Nullable String badgeOverride) throws Exception {
         Builder builder = new Builder(context, "test-shortcut")
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
new file mode 100644
index 0000000..bae74c8
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.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.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 iconProvider: IconProvider
+    @Mock private lateinit var baseIconCache: BaseIconCache
+    @Mock private lateinit var cacheDb: IconDB
+    @Mock private lateinit var workerHandler: Handler
+
+    @Captor private lateinit var deleteCaptor: ArgumentCaptor<String>
+
+    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)
+        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 `keeps correct icons irrespective of call order`() {
+        val obj1 = TestCachedObject(1).apply { addToCursor(cursor) }
+        val obj2 = TestCachedObject(2).apply { addToCursor(cursor) }
+
+        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)
+            )
+    }
+
+    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())
+    }
+}
+
+/** 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/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
index 370af0c..ce04682 100644
--- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -118,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)
@@ -170,7 +160,7 @@
                 eq(IntArray.wrap(*nonEmptyScreenIds.toIntArray())),
                 eq(IntArray()),
                 eq(1),
-                eq(1)
+                eq(1),
             )
     }
 
@@ -180,7 +170,7 @@
      */
     private fun testAddItems(
         nonEmptyScreenIds: List<Int>,
-        vararg itemsToAdd: WorkspaceItemInfo
+        vararg itemsToAdd: WorkspaceItemInfo,
     ): List<AddedItem> {
         setupWorkspaces(nonEmptyScreenIds)
         val task = newTask(*itemsToAdd)
@@ -217,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/src/com/android/launcher3/model/AsyncBindingTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
similarity index 98%
rename from tests/src/com/android/launcher3/model/AsyncBindingTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
index af367a8..ba59253 100644
--- a/tests/src/com/android/launcher3/model/AsyncBindingTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
@@ -136,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()
@@ -201,7 +201,7 @@
             pendingTasks: RunnableList,
             onCompleteSignal: RunnableList,
             workspaceItemCount: Int,
-            isBindSync: Boolean
+            isBindSync: Boolean,
         ) {
             this.pendingTasks = pendingTasks
             this.onCompleteSignal = onCompleteSignal
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
similarity index 84%
rename from tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
rename to tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 328558d..600af42 100644
--- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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 static android.os.Process.myUserHandle;
@@ -11,10 +26,13 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
+import android.content.pm.PackageInstaller;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -46,8 +64,7 @@
 @RunWith(AndroidJUnit4.class)
 public class CacheDataUpdatedTaskTest {
 
-    @Rule(order = 0)
-    public TestRule testStabilityRule = new TestStabilityRule();
+    @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";
@@ -128,10 +145,13 @@
     @Test
     public void testSessionUpdate_updates_pending_apps() {
         // Run on model executor so that no other task runs in the middle.
+        PackageInstaller.SessionInfo sessionInfo = ApplicationProvider.getApplicationContext()
+                        .getPackageManager().getPackageInstaller().getSessionInfo(mSession1);
+        assertNotNull(sessionInfo);
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
             LauncherAppState.getInstance(mContext).getIconCache().updateSessionCache(
                     new PackageUserKey(PENDING_APP_1, myUserHandle()),
-                    mContext.getPackageManager().getPackageInstaller().getSessionInfo(mSession1));
+                    sessionInfo);
 
             // Clear all icons from apps list so that its easy to check what was updated
             allItems().forEach(wi -> wi.bitmap = BitmapInfo.LOW_RES_INFO);
diff --git a/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
similarity index 94%
rename from tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
rename to tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index 10785f7..1e2431f 100644
--- a/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -22,11 +22,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 
-import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -50,7 +50,7 @@
 public class DefaultLayoutProviderTest {
 
     private LauncherModelHelper mModelHelper;
-    private Context mTargetContext;
+    private LauncherModelHelper.SandboxModelContext mTargetContext;
 
     @Before
     public void setUp() {
@@ -114,8 +114,10 @@
         SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
         params.setAppPackageName(pendingAppPkg);
         params.setAppIcon(BitmapInfo.LOW_RES_ICON);
+        params.installerPackageName = ApplicationProvider.getApplicationContext().getPackageName();
 
-        PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
+        PackageInstaller installer = ApplicationProvider.getApplicationContext().getPackageManager()
+                .getPackageInstaller();
         installer.createSession(params);
 
         writeLayoutAndLoad(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
diff --git a/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
similarity index 96%
rename from tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
index aadf72e..9cc380e 100644
--- a/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
@@ -21,6 +21,7 @@
 import android.content.Intent
 import android.content.pm.PackageInstaller.SessionInfo
 import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
@@ -34,13 +35,16 @@
 import com.android.launcher3.util.PackageUserKey
 import junit.framework.Assert.assertEquals
 import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
+@RunWith(AndroidJUnit4::class)
 class FirstScreenBroadcastHelperTest {
+
     private val context = spy(InstrumentationRegistry.getInstrumentation().targetContext)
     private val mockPmHelper = mock<PackageManagerHelper>()
     private val expectedAppPackage = "appPackageExpected"
@@ -63,7 +67,7 @@
                 container = CONTAINER_HOTSEAT
                 intent = expectedIntent
             },
-            LauncherAppWidgetInfo().apply { providerName = expectedComponentName }
+            LauncherAppWidgetInfo().apply { providerName = expectedComponentName },
         )
 
     @Test
@@ -82,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
@@ -91,7 +95,7 @@
                 packageManagerHelper = mockPmHelper,
                 firstScreenItems = firstScreenItems,
                 userKeyToSessionMap = sessionInfoMap,
-                allWidgets = listOf()
+                allWidgets = listOf(),
             )
 
         // Then
@@ -101,7 +105,7 @@
                     installerPackage = expectedInstallerPackage,
                     pendingWorkspaceItems = mutableSetOf(expectedAppPackage),
                     pendingHotseatItems = mutableSetOf(expectedAppPackage),
-                    pendingWidgetItems = mutableSetOf(expectedAppPackage)
+                    pendingWidgetItems = mutableSetOf(expectedAppPackage),
                 )
             )
 
@@ -126,7 +130,7 @@
                             providerName = expectedComponentName
                             screenId = 0
                         }
-                    )
+                    ),
             )
 
         // Then
@@ -136,7 +140,7 @@
                     installerPackage = expectedInstallerPackage,
                     installedHotseatItems = mutableSetOf(expectedAppPackage),
                     installedWorkspaceItems = mutableSetOf(expectedAppPackage),
-                    firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage)
+                    firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage),
                 )
             )
         assertEquals(expectedResult, actualResult)
@@ -171,8 +175,8 @@
                         LauncherAppWidgetInfo().apply {
                             providerName = unexpectedComponentName
                             screenId = 0
-                        }
-                    )
+                        },
+                    ),
             )
 
         // Then
@@ -183,7 +187,7 @@
                     installedHotseatItems = mutableSetOf(),
                     installedWorkspaceItems = mutableSetOf(),
                     firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage),
-                    secondaryScreenInstalledWidgets = mutableSetOf(expectedAppPackage2)
+                    secondaryScreenInstalledWidgets = mutableSetOf(expectedAppPackage2),
                 )
             )
         assertEquals(expectedResult, actualResult)
@@ -217,7 +221,7 @@
                 packageManagerHelper = mockPmHelper,
                 firstScreenItems = firstScreenItems,
                 userKeyToSessionMap = sessionInfoMap,
-                allWidgets = listOf()
+                allWidgets = listOf(),
             )
 
         // Then
@@ -225,7 +229,7 @@
             listOf(
                 FirstScreenBroadcastModel(
                     installerPackage = expectedInstallerPackage,
-                    pendingCollectionItems = mutableSetOf(expectedAppPackage)
+                    pendingCollectionItems = mutableSetOf(expectedAppPackage),
                 )
             )
         assertEquals(expectedResult, actualResult)
@@ -252,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
@@ -327,7 +331,7 @@
                     installedWorkspaceItems = mutableSetOf("installedWorkspaceItems"),
                     installedHotseatItems = mutableSetOf("installedHotseatItems"),
                     firstScreenInstalledWidgets = mutableSetOf("firstScreenInstalledWidgetItems"),
-                    secondaryScreenInstalledWidgets = mutableSetOf("secondaryInstalledWidgetItems")
+                    secondaryScreenInstalledWidgets = mutableSetOf("secondaryInstalledWidgetItems"),
                 )
             )
         val expectedPendingIntent =
@@ -335,7 +339,7 @@
                 context,
                 0 /* requestCode */,
                 Intent(),
-                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
             )
 
         // When
@@ -347,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/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
similarity index 93%
rename from tests/src/com/android/launcher3/model/FolderIconLoadTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
index c4a4c9b..7cd5da4 100644
--- a/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
@@ -18,6 +18,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.waitForUpdateHandlerToFinish
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.LauncherLayoutBuilder
@@ -26,7 +27,6 @@
 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.Test
@@ -36,6 +36,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class FolderIconLoadTest {
+
     private lateinit var modelHelper: LauncherModelHelper
 
     private val uniqueActivities =
@@ -53,7 +54,7 @@
             TEST_ACTIVITY11,
             TEST_ACTIVITY12,
             TEST_ACTIVITY13,
-            TEST_ACTIVITY14
+            TEST_ACTIVITY14,
         )
 
     @Before
@@ -141,13 +142,9 @@
         // The first load initializes the DB, load again so that icons are now used from the DB
         // Wait for the icon cache to be updated and then reload
         val app = LauncherAppState.getInstance(modelHelper.sandboxContext)
-        val cache = app.iconCache
-        while (cache.isIconUpdateInProgress) {
-            val wait = CountDownLatch(1)
-            Executors.MODEL_EXECUTOR.handler.postDelayed({ wait.countDown() }, 10)
-            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 75%
rename from tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index 761f06d..7933331 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -22,13 +22,16 @@
 import android.database.sqlite.SQLiteDatabase
 import android.graphics.Point
 import android.os.Process
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
 import com.android.launcher3.LauncherSettings.Favorites.*
-import com.android.launcher3.model.GridSizeMigrationUtil.DbReader
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.provider.LauncherDbUtils
 import com.android.launcher3.util.LauncherModelHelper
@@ -38,14 +41,13 @@
 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
-    private lateinit var validPackages: Set<String>
     private lateinit var idp: InvariantDeviceProfile
     private lateinit var dbHelper: DatabaseHelper
     private lateinit var db: SQLiteDatabase
@@ -68,24 +70,10 @@
             DatabaseHelper(
                 context,
                 null,
-                UserCache.INSTANCE.get(context)::getSerialNumberForUser
+                UserCache.INSTANCE.get(context)::getSerialNumberForUser,
             ) {}
         db = dbHelper.writableDatabase
 
-        validPackages =
-            setOf(
-                testPackage1,
-                testPackage2,
-                testPackage3,
-                testPackage4,
-                testPackage5,
-                testPackage6,
-                testPackage7,
-                testPackage8,
-                testPackage9,
-                testPackage10
-            )
-
         idp = InvariantDeviceProfile.INSTANCE[context]
         val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
         LauncherDbUtils.dropTable(db, TMP_TABLE)
@@ -97,9 +85,22 @@
         modelHelper.destroy()
     }
 
-    /** Old migration logic, should be modified once is not needed anymore */
     @Test
     @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationRefactorFlagOn() {
+        testMigration()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationRefactorFlagOff() {
+        testMigration()
+    }
+
+    /** Old migration logic, should be modified once is not needed anymore */
+    @Throws(Exception::class)
     fun testMigration() {
         // Src Hotseat icons
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
@@ -126,17 +127,36 @@
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
         idp.numRows = 4
-        val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
-        val destReader = DbReader(db, TABLE_NAME, context, validPackages)
-        GridSizeMigrationUtil.migrate(
-            dbHelper,
-            srcReader,
-            destReader,
-            idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp)
-        )
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
+        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 =
@@ -147,9 +167,8 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons)
 
@@ -178,9 +197,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         intentIndex = c.getColumnIndex(INTENT)
         val cellXIndex = c.getColumnIndex(CELLX)
@@ -204,9 +222,22 @@
         assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2))
     }
 
-    /** Old migration logic, should be modified once is not needed anymore */
     @Test
     @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationBackAndForthRefactorFlagOn() {
+        testMigrationBackAndForth()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationBackAndForthRefactorFlagOff() {
+        testMigrationBackAndForth()
+    }
+
+    /** Old migration logic, should be modified once is not needed anymore */
+    @Throws(Exception::class)
     fun testMigrationBackAndForth() {
         // Hotseat items in grid A
         // 1 2 _ 3 4
@@ -238,18 +269,37 @@
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
         idp.numRows = 4
-        val readerGridA = DbReader(db, TMP_TABLE, context, validPackages)
-        val readerGridB = DbReader(db, TABLE_NAME, context, validPackages)
+        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 =
@@ -260,15 +310,14 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         // Expected hotseat items in grid B
         // 2 1 3 4
         verifyHotseat(
             c,
             idp,
-            mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList()
+            mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList(),
         )
 
         // Check workspace items in grid B
@@ -280,9 +329,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         var locMap = parseLocMap(c)
         // Expected items in grid B
         // _ _ _ _
@@ -299,15 +347,8 @@
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 2, testPackage9)
 
         // migrate from B -> A
-        GridSizeMigrationUtil.migrate(
-            dbHelper,
-            readerGridB,
-            readerGridA,
-            5,
-            Point(5, 5),
-            DeviceGridState(idp),
-            DeviceGridState(context)
-        )
+        migrateGrid(dbHelper, readerGridB, readerGridA, 5, 5, 5)
+
         // Check hotseat items in grid A
         c =
             db.query(
@@ -317,15 +358,14 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         // Expected hotseat items in grid A
         // 1 2 _ 3 4
         verifyHotseat(
             c,
             idp,
-            mutableListOf(testPackage1, testPackage2, null, testPackage3, testPackage4).toList()
+            mutableListOf(testPackage1, testPackage2, null, testPackage3, testPackage4).toList(),
         )
 
         // Check workspace items in grid A
@@ -337,9 +377,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         locMap = parseLocMap(c)
         // Expected workspace items in grid A
         // _ _ _ _ _
@@ -360,14 +399,13 @@
         db.delete(TMP_TABLE, "$_ID=7", null)
 
         // migrate from A -> B
-        GridSizeMigrationUtil.migrate(
+        migrateGrid(
             dbHelper,
             readerGridA,
             readerGridB,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp)
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Check hotseat items in grid B
@@ -379,15 +417,14 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         // Expected hotseat items in grid B
         // 2 1 3 4
         verifyHotseat(
             c,
             idp,
-            mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList()
+            mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList(),
         )
 
         // Check workspace items in grid B
@@ -399,9 +436,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         locMap = parseLocMap(c)
         // Expected workspace items in grid B
         // _ _ _ _
@@ -415,6 +451,44 @@
         assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
     }
 
+    private fun migrateGrid(
+        dbHelper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        destHotseatSize: Int,
+        pointX: Int,
+        pointY: Int,
+    ) {
+        if (Flags.gridMigrationRefactor()) {
+            var gridSizeMigrationLogic = GridSizeMigrationLogic()
+            val idsInUse = mutableListOf<Int>()
+            gridSizeMigrationLogic.migrateHotseat(
+                idp.numDatabaseHotseatIcons,
+                srcReader,
+                destReader,
+                dbHelper,
+                idsInUse,
+            )
+            gridSizeMigrationLogic.migrateWorkspace(
+                srcReader,
+                destReader,
+                dbHelper,
+                Point(idp.numColumns, idp.numRows),
+                idsInUse,
+            )
+        } else {
+            GridSizeMigrationDBController.migrate(
+                dbHelper,
+                srcReader,
+                destReader,
+                destHotseatSize,
+                Point(pointX, pointY),
+                DeviceGridState(idp),
+                DeviceGridState(context),
+            )
+        }
+    }
+
     private fun verifyHotseat(c: Cursor, idp: InvariantDeviceProfile, expected: List<String?>) {
         assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons)
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -444,6 +518,17 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateToLargerHotseatRefactorFlagOn() {
+        migrateToLargerHotseat()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateToLargerHotseatRefactorFlagOff() {
+        migrateToLargerHotseat()
+    }
+
     fun migrateToLargerHotseat() {
         val srcHotseatItems =
             intArrayOf(
@@ -455,7 +540,7 @@
                     0,
                     testPackage1,
                     1,
-                    TMP_TABLE
+                    TMP_TABLE,
                 ),
                 addItem(
                     ITEM_TYPE_DEEP_SHORTCUT,
@@ -465,7 +550,7 @@
                     0,
                     testPackage2,
                     2,
-                    TMP_TABLE
+                    TMP_TABLE,
                 ),
                 addItem(
                     ITEM_TYPE_APPLICATION,
@@ -475,7 +560,7 @@
                     0,
                     testPackage3,
                     3,
-                    TMP_TABLE
+                    TMP_TABLE,
                 ),
                 addItem(
                     ITEM_TYPE_DEEP_SHORTCUT,
@@ -485,23 +570,22 @@
                     0,
                     testPackage4,
                     4,
-                    TMP_TABLE
-                )
+                    TMP_TABLE,
+                ),
             )
         val numSrcDatabaseHotseatIcons = srcHotseatItems.size
         idp.numDatabaseHotseatIcons = 6
         idp.numColumns = 4
         idp.numRows = 4
-        val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
-        val destReader = DbReader(db, TABLE_NAME, context, validPackages)
-        GridSizeMigrationUtil.migrate(
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp)
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Check hotseat items
@@ -513,9 +597,8 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         assertThat(c.count.toLong()).isEqualTo(numSrcDatabaseHotseatIcons.toLong())
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -540,6 +623,17 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerHotseatRefactorFlagOn() {
+        migrateFromLargerHotseat()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerHotseatRefactorFlagOff() {
+        migrateFromLargerHotseat()
+    }
+
     fun migrateFromLargerHotseat() {
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
         addItem(ITEM_TYPE_DEEP_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
@@ -550,16 +644,15 @@
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
         idp.numRows = 4
-        val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
-        val destReader = DbReader(db, TABLE_NAME, context, validPackages)
-        GridSizeMigrationUtil.migrate(
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp)
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Check hotseat items
@@ -571,9 +664,8 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         assertThat(c.count.toLong()).isEqualTo(idp.numDatabaseHotseatIcons.toLong())
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -598,11 +690,24 @@
         c.close()
     }
 
+    @Test
+    @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromSmallerGridBigDifferenceRefactorFlagOn() {
+        migrateFromSmallerGridBigDifference()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromSmallerGridBigDifferenceRefactorFlagOff() {
+        migrateFromSmallerGridBigDifference()
+    }
+
     /**
      * Migrating from a smaller grid to a large one should reflow the pages if the column difference
      * is more than 2
      */
-    @Test
     @Throws(Exception::class)
     fun migrateFromSmallerGridBigDifference() {
         enableNewMigrationLogic("2,2")
@@ -617,16 +722,15 @@
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 5
         idp.numRows = 5
-        val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
-        val destReader = DbReader(db, TABLE_NAME, context, validPackages)
-        GridSizeMigrationUtil.migrate(
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp)
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Get workspace items
@@ -638,9 +742,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         val intentIndex = c.getColumnIndex(INTENT)
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -662,9 +765,22 @@
         assertThat(locMap[testPackage5]).isEqualTo(0)
     }
 
-    /** Migrating from a larger grid to a smaller, we reflow from page 0 */
     @Test
     @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerGridRefactorFlagOn() {
+        migrateFromLargerGrid()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerGridRefactorFlagOff() {
+        migrateFromLargerGrid()
+    }
+
+    /** Migrating from a larger grid to a smaller, we reflow from page 0 */
+    @Throws(Exception::class)
     fun migrateFromLargerGrid() {
         enableNewMigrationLogic("5,5")
 
@@ -678,16 +794,15 @@
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
         idp.numRows = 4
-        val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
-        val destReader = DbReader(db, TABLE_NAME, context, validPackages)
-        GridSizeMigrationUtil.migrate(
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp)
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Get workspace items
@@ -699,9 +814,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         val intentIndex = c.getColumnIndex(INTENT)
         val screenIndex = c.getColumnIndex(SCREEN)
 
@@ -732,7 +846,7 @@
         container: Int,
         x: Int,
         y: Int,
-        packageName: String?
+        packageName: String?,
     ): Int {
         return addItem(
             type,
@@ -742,7 +856,7 @@
             y,
             packageName,
             dbHelper.generateNewItemId(),
-            TABLE_NAME
+            TABLE_NAME,
         )
     }
 
@@ -754,7 +868,7 @@
         y: Int,
         packageName: String?,
         id: Int,
-        tableName: String
+        tableName: String,
     ): Int {
         val values = ContentValues()
         values.put(_ID, id)
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/model/LoaderCursorTest.java
rename to tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
diff --git a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
similarity index 88%
rename from tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
rename to tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index 4ba61ac..0f1fc00 100644
--- a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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 static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
new file mode 100644
index 0000000..ae4ff04
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -0,0 +1,261 @@
+/*
+ * 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.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.WidgetSections
+import com.android.launcher3.widget.WidgetSections.NO_CATEGORY
+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
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+@RunWith(AndroidJUnit4::class)
+class WidgetsModelTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @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
+    private lateinit var underTest: WidgetsModel
+
+    private var widgetSectionCategory: Int = 0
+    private lateinit var appAPackage: String
+
+    @Before
+    fun setUp() {
+        val appContext: Context = ApplicationProvider.getApplicationContext()
+        idp = InvariantDeviceProfile.INSTANCE[appContext]
+
+        context =
+            object : ActivityContextWrapper(ApplicationProvider.getApplicationContext()) {
+                override fun getSystemService(name: String): Any? {
+                    if (name == "appwidget") {
+                        return appWidgetManager
+                    }
+                    return super.getSystemService(name)
+                }
+
+                override fun getDeviceProfile(): DeviceProfile {
+                    return idp.getDeviceProfile(applicationContext).copy(applicationContext)
+                }
+            }
+
+        whenever(iconCacheMock.getTitleNoCache(any<LauncherAppWidgetProviderInfo>()))
+            .thenReturn("title")
+        whenever(app.iconCache).thenReturn(iconCacheMock)
+        whenever(app.context).thenReturn(context)
+        whenever(app.invariantDeviceProfile).thenReturn(idp)
+
+        val widgetToCategoryEntry: Map.Entry<ComponentName, IntSet> =
+            WidgetSections.getWidgetsToCategory(context).entries.first()
+        widgetSectionCategory = widgetToCategoryEntry.value.first()
+        val appAWidgetComponent = widgetToCategoryEntry.key
+        appAPackage = appAWidgetComponent.packageName
+
+        whenever(appWidgetManager.getInstalledProvidersForProfile(any()))
+            .thenReturn(
+                listOf(
+                    // First widget from widget sections xml
+                    createAppWidgetProviderInfo(appAWidgetComponent),
+                    // A widget that belongs to same package as the widget from widget sections
+                    // xml, but, because it's not mentioned in xml, it would be included in its
+                    // own package section.
+                    createAppWidgetProviderInfo(
+                        ComponentName.createRelative(appAPackage, APP_A_TEST_WIDGET_NAME)
+                    ),
+                    // A widget in different package (none of that app's widgets are in widget
+                    // sections xml)
+                    createAppWidgetProviderInfo(AppBTestWidgetComponent),
+                )
+            )
+
+        val userCache = spy(UserCache.INSTANCE.get(context))
+        whenever(userCache.userProfiles).thenReturn(listOf(UserHandle.CURRENT))
+
+        underTest = WidgetsModel()
+    }
+
+    @Test
+    fun widgetsByPackage_treatsWidgetSectionsAsSeparatePackageItems() {
+        loadWidgets()
+
+        val packages: Map<PackageItemInfo, List<WidgetItem>> = underTest.widgetsByPackageItem
+
+        // expect 3 package items
+        // one for the custom section with widget from appA
+        // one for package section for second widget from appA (that wasn't listed in xml)
+        // and one for package section for appB
+        assertThat(packages).hasSize(3)
+
+        // Each package item when used as a key is distinct (i.e. even if appA is split into custom
+        // package and owner package section, each of them is a distinct key). This ensures that
+        // clicking on a custom widget section doesn't take user to app package section.
+        val distinctPackageUserKeys =
+            packages.map { PackageUserKey.fromPackageItemInfo(it.key) }.distinct()
+        assertThat(distinctPackageUserKeys).hasSize(3)
+
+        val customSections = packages.filter { it.key.widgetCategory == widgetSectionCategory }
+        assertThat(customSections).hasSize(1)
+        val widgetsInCustomSection = customSections.entries.first().value
+        assertThat(widgetsInCustomSection).hasSize(1)
+
+        val packageSections = packages.filter { it.key.widgetCategory == NO_CATEGORY }
+        assertThat(packageSections).hasSize(2)
+
+        // App A's package section
+        val appAPackageSection = packageSections.filter { it.key.packageName == appAPackage }
+        assertThat(appAPackageSection).hasSize(1)
+        val widgetsInAppASection = appAPackageSection.entries.first().value
+        assertThat(widgetsInAppASection).hasSize(1)
+
+        // App B's package section
+        val appBPackageSection =
+            packageSections.filter { it.key.packageName == AppBTestWidgetComponent.packageName }
+        assertThat(appBPackageSection).hasSize(1)
+        val widgetsInAppBSection = appBPackageSection.entries.first().value
+        assertThat(widgetsInAppBSection).hasSize(1)
+    }
+
+    @Test
+    fun widgetComponentMap_returnsWidgets() {
+        loadWidgets()
+
+        val widgetsByComponentKey: Map<ComponentKey, WidgetItem> = underTest.widgetsByComponentKey
+
+        assertThat(widgetsByComponentKey).hasSize(3)
+        widgetsByComponentKey.forEach { entry ->
+            assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
+        }
+    }
+
+    @Test
+    fun widgets_noData_returnsEmpty() {
+        // no loadWidgets()
+
+        assertThat(underTest.widgetsByComponentKey).isEmpty()
+    }
+
+    @Test
+    fun getWidgetsByPackageItem_returnsACopyOfMap() {
+        loadWidgets()
+
+        val latch = CountDownLatch(1)
+        Executors.MODEL_EXECUTOR.execute {
+            var update = true
+
+            // each "widgetsByPackageItem" read returns a different copy of the map held internally.
+            // Modifying one shouldn't impact another.
+            for ((_, _) in underTest.widgetsByPackageItem.entries) {
+                underTest.widgetsByPackageItem.clear()
+                if (update) { // trigger update
+                    update = false
+                    // Similarly, model could update its code independently while a client is
+                    // iterating on the list.
+                    underTest.update(app, /* packageUser= */ null)
+                }
+            }
+
+            latch.countDown()
+        }
+        if (!latch.await(LOAD_WIDGETS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+            fail("Timed out waiting for test")
+        }
+
+        // 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 {
+            underTest.update(app, /* packageUser= */ null)
+            latch.countDown()
+        }
+        if (!latch.await(LOAD_WIDGETS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+            fail("Timed out waiting widgets to load")
+        }
+    }
+
+    companion object {
+        // Another widget within app A
+        private const val APP_A_TEST_WIDGET_NAME = "MyProvider"
+
+        private val AppBTestWidgetComponent: ComponentName =
+            ComponentName.createRelative("com.test.package", "TestProvider")
+
+        private const val LOAD_WIDGETS_TIMEOUT_SECONDS = 2L
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
similarity index 79%
rename from tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index 6cf3b19..ed8b397 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -25,10 +25,9 @@
 import android.content.pm.ShortcutInfo
 import android.os.Process
 import android.os.UserHandle
-import android.platform.test.annotations.EnableFlags
 import android.util.LongSparseArray
-import com.android.dx.mockito.inline.extended.ExtendedMockito
-import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
@@ -36,17 +35,16 @@
 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.Utilities
 import com.android.launcher3.Utilities.EMPTY_PERSON_ARRAY
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
 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
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
-import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
 import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_UI_NOT_READY
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.pm.UserCache
@@ -57,11 +55,11 @@
 import com.android.launcher3.util.UserIconInfo
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
 import com.android.launcher3.widget.WidgetInflater
-import com.android.launcher3.widget.WidgetSections
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
 import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
@@ -74,8 +72,8 @@
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
-import org.mockito.quality.Strictness
 
+@RunWith(AndroidJUnit4::class)
 class WorkspaceItemProcessorTest {
 
     @Mock private lateinit var mockIconRequestInfo: IconRequestInfo<WorkspaceItemInfo>
@@ -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,10 +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 {
@@ -134,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
@@ -192,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,
@@ -211,7 +211,7 @@
             isSdCardReady = isSdCardReady,
             shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
             installingPkgs = installingPkgs,
-            allDeepShortcuts = allDeepShortcuts
+            allDeepShortcuts = allDeepShortcuts,
         )
 
     @Test
@@ -350,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())
     }
@@ -385,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())
     }
@@ -411,7 +412,7 @@
         verify(mockCursor)
             .markDeleted(
                 "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
-                "shortcut_not_found"
+                "shortcut_not_found",
             )
     }
 
@@ -450,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())
     }
@@ -548,7 +550,7 @@
         val inflationResult =
             WidgetInflater.InflationResult(
                 type = WidgetInflater.TYPE_REAL,
-                widgetInfo = expectedWidgetProviderInfo
+                widgetInfo = expectedWidgetProviderInfo,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
@@ -606,7 +608,7 @@
         val inflationResult =
             WidgetInflater.InflationResult(
                 type = WidgetInflater.TYPE_PENDING,
-                widgetInfo = mockProviderInfo
+                widgetInfo = mockProviderInfo,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
@@ -661,156 +663,14 @@
         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,
             )
     }
 
     @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)
-                        .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)
-            }
-        } finally {
-            mockitoSession.finishMocking()
-        }
-    }
-
-    @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)
-
-            // When
-            itemProcessorUnderTest.processItem()
-
-            // Then
-            verify(mockCursor).checkAndAddItem(any(), any())
-        } finally {
-            mockitoSession.finishMocking()
-        }
-    }
-
-    @Test
     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
@@ -829,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/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
similarity index 98%
rename from tests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
index b3d02be..dd03eee 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
@@ -49,7 +49,7 @@
                 mExistingScreens,
                 mNewScreens,
                 spanX,
-                spanY
+                spanY,
             )
             .let { NewItemSpace.fromIntArray(it) }
 
@@ -59,7 +59,7 @@
                     newItemSpace.cellX,
                     newItemSpace.cellY,
                     spanX,
-                    spanY
+                    spanY,
                 )
             )
             .isTrue()
@@ -168,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
new file mode 100644
index 0000000..15a9964
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.pm
+
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.os.Build
+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.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.PackageUserKey
+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.doNothing
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionTrackerTest {
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val mockInstallSessionHelper: InstallSessionHelper = mock()
+    private val mockCallback: InstallSessionTracker.Callback = mock()
+    private val mockPackageInstaller: PackageInstaller = mock()
+
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = launcherModelHelper.sandboxContext
+
+    lateinit var launcherApps: LauncherApps
+    lateinit var installSessionTracker: InstallSessionTracker
+
+    @Before
+    fun setup() {
+        launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+        installSessionTracker =
+            InstallSessionTracker(
+                mockInstallSessionHelper,
+                mockCallback,
+                mockPackageInstaller,
+                launcherApps,
+            )
+    }
+
+    @After
+    fun teardown() {
+        launcherModelHelper.destroy()
+    }
+
+    @Test
+    fun `onCreated triggers callbacks for setting up new install session`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onCreated(expectedSessionId)
+        // Then
+        verify(mockCallback).onInstallSessionCreated(any())
+        verify(mockCallback).onUpdateSessionDisplay(expectedPackageKey, expectedSession)
+        verify(mockInstallSessionHelper).tryQueuePromiseAppIcon(expectedSession)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+    fun `onCreated for unarchival triggers onPackageStateChanged`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            spy(PackageInstaller.SessionInfo()).apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+                whenever(isUnarchival).thenReturn(true)
+            }
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onCreated(expectedSessionId)
+        // Then
+        verify(mockCallback).onPackageStateChanged(any())
+    }
+
+    @Test
+    fun `onFinished triggers onPackageStateChanged if session found in cache`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        whenever(mockInstallSessionHelper.activeSessions)
+            .thenReturn(hashMapOf(expectedPackageKey to expectedSession))
+        // When
+        installSessionTracker.onFinished(expectedSessionId, /* success */ true)
+        // Then
+        verify(mockCallback).onPackageStateChanged(any())
+    }
+
+    @Test
+    fun `onFinished failure calls onSessionFailure and promise icon removal for existing icon`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedPackage = "appPackageName"
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = expectedPackage
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey(expectedPackage, UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        whenever(mockInstallSessionHelper.activeSessions)
+            .thenReturn(hashMapOf(expectedPackageKey to expectedSession))
+        whenever(mockInstallSessionHelper.promiseIconAddedForId(expectedSessionId)).thenReturn(true)
+        // When
+        installSessionTracker.onFinished(expectedSessionId, /* success */ false)
+        // Then
+        verify(mockCallback).onSessionFailure(expectedPackage, expectedPackageKey.mUser)
+        verify(mockInstallSessionHelper).removePromiseIconId(expectedSessionId)
+    }
+
+    @Test
+    fun `onProgressChanged triggers onPackageStateChanged if verified session found`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onProgressChanged(expectedSessionId, /* progress */ 50f)
+        // Then
+        verify(mockCallback).onPackageStateChanged(any())
+    }
+
+    @Test
+    fun `onBadgingChanged triggers session display update and queues promise icon if verified`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onBadgingChanged(expectedSessionId)
+        // Then
+        verify(mockCallback).onUpdateSessionDisplay(expectedPackageKey, expectedSession)
+        verify(mockInstallSessionHelper).tryQueuePromiseAppIcon(expectedSession)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    fun `register triggers registerPackageInstallerSessionCallback for versions from Q`() {
+        // Given
+        doNothing()
+            .whenever(launcherApps)
+            .registerPackageInstallerSessionCallback(MODEL_EXECUTOR, installSessionTracker)
+        // When
+        installSessionTracker.register()
+        // Then
+        verify(launcherApps)
+            .registerPackageInstallerSessionCallback(MODEL_EXECUTOR, installSessionTracker)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    fun `unregister triggers unregisterPackageInstallerSessionCallback for versions from Q`() {
+        // Given
+        doNothing()
+            .whenever(launcherApps)
+            .unregisterPackageInstallerSessionCallback(installSessionTracker)
+        // When
+        installSessionTracker.unregister()
+        // Then
+        verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/UserCacheTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/UserCacheTest.kt
new file mode 100644
index 0000000..5f08c31
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/UserCacheTest.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.pm
+
+import android.os.Process.myUserHandle
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.android.launcher3.util.UserIconInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UserCacheTest {
+
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = launcherModelHelper.sandboxContext
+    private lateinit var userCache: UserCache
+
+    @Before
+    fun setup() {
+        userCache = UserCache.getInstance(sandboxContext)
+    }
+
+    @After
+    fun teardown() {
+        launcherModelHelper.destroy()
+    }
+
+    @Test
+    fun `getBadgeDrawable only returns a UserBadgeDrawable given a user in the cache`() {
+        // Given
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(myUserHandle(), expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualDrawable = UserCache.getBadgeDrawable(sandboxContext, myUserHandle())
+        val unexpectedDrawable = UserCache.getBadgeDrawable(sandboxContext, UserHandle(66))
+        // Then
+        assertThat(actualDrawable).isNotNull()
+        assertThat(unexpectedDrawable).isNull()
+    }
+
+    @Test
+    fun `getPreInstallApps returns list of pre installed apps given a user`() {
+        // Given
+        val expectedApps = listOf("Google")
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToPreInstallCache(myUserHandle(), expectedApps)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualApps = userCache.getPreInstallApps(myUserHandle())
+        // Then
+        assertThat(actualApps).isEqualTo(expectedApps)
+    }
+
+    @Test
+    fun `getUserProfiles returns copy of UserCache profiles`() {
+        // Given
+        val expectedProfiles = listOf(myUserHandle())
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(myUserHandle(), expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualProfiles = userCache.userProfiles
+        // Then
+        assertThat(actualProfiles).isEqualTo(expectedProfiles)
+    }
+
+    @Test
+    fun `getUserForSerialNumber returns user key matching given entry serial number`() {
+        // Given
+        val expectedSerial = 42L
+        val expectedProfile = UserHandle(42)
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN, expectedSerial)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(expectedProfile, expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualProfile = userCache.getUserForSerialNumber(expectedSerial)
+        // Then
+        assertThat(actualProfile).isEqualTo(expectedProfile)
+    }
+
+    @Test
+    fun `getUserInfo returns cached UserIconInfo given user key`() {
+        // Given
+        val expectedProfile = UserHandle(1)
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(expectedProfile, expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualIconInfo = userCache.getUserInfo(expectedProfile)
+        // Then
+        assertThat(actualIconInfo).isEqualTo(expectedIconInfo)
+    }
+
+    @Test
+    fun `getSerialNumberForUser returns cached UserIconInfo serial number given user key`() {
+        // Given
+        val expectedSerial = 42L
+        val expectedProfile = UserHandle(1)
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK, expectedSerial)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(expectedProfile, expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualSerial = userCache.getSerialNumberForUser(expectedProfile)
+        // Then
+        assertThat(actualSerial).isEqualTo(expectedSerial)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index b3675a6..d0c168a 100644
--- a/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -101,7 +101,7 @@
         mMockController = Mockito.mock(ModelDbController.class);
         mMockDb = mock(SQLiteDatabase.class);
         mMockCursor = mock(Cursor.class);
-        mPrefs = new LauncherPrefs(mContext);
+        mPrefs = LauncherPrefs.get(mContext);
         mMockRestoreEventLogger = mock(LauncherRestoreEventLogger.class);
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java b/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java
deleted file mode 100644
index 10e0be8..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.launcher3.settings;
-
-import static androidx.preference.PreferenceFragmentCompat.ARG_PREFERENCE_ROOT;
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem;
-import static androidx.test.espresso.intent.Intents.intended;
-import static androidx.test.espresso.intent.matcher.BundleMatchers.hasEntry;
-import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent;
-import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
-import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-
-import static com.android.launcher3.settings.SettingsActivity.DEVELOPER_OPTIONS_KEY;
-import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARGS;
-import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ROOT_KEY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.equalTo;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-
-import androidx.test.core.app.ActivityScenario;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.intent.Intents;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.launcher3.R;
-import com.android.systemui.shared.plugins.PluginPrefs;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class SettingsActivityTest {
-
-    private Context mApplicationContext;
-
-    @Before
-    public void setUp() {
-        mApplicationContext = ApplicationProvider.getApplicationContext();
-        Intents.init();
-    }
-
-    @After
-    public void tearDown() {
-        Intents.release();
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_aboutTap_launchesActivity() {
-        ActivityScenario.launch(SettingsActivity.class);
-        onView(withId(R.id.recycler_view)).perform(
-                actionOnItem(hasDescendant(withText("About")), click()));
-
-        intended(allOf(
-                hasComponent(SettingsActivity.class.getName()),
-                hasExtra(
-                        equalTo(EXTRA_FRAGMENT_ARGS),
-                        hasEntry(ARG_PREFERENCE_ROOT, "about_screen"))));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_developerOptionsTap_launchesActivityWithFragment() {
-        PluginPrefs.setHasPlugins(mApplicationContext);
-        ActivityScenario.launch(SettingsActivity.class);
-        onView(withId(R.id.recycler_view)).perform(
-                actionOnItem(hasDescendant(withText("Developer Options")), click()));
-
-        intended(allOf(
-                hasComponent(SettingsActivity.class.getName()),
-                hasExtra(EXTRA_FRAGMENT_ROOT_KEY, DEVELOPER_OPTIONS_KEY)));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_aboutScreenIntent() {
-        Bundle fragmentArgs = new Bundle();
-        fragmentArgs.putString(ARG_PREFERENCE_ROOT, "about_screen");
-
-        Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
-                .putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs);
-        ActivityScenario.launch(intent);
-
-        onView(withText("About")).check(matches(isDisplayed()));
-        onView(withText("Version")).check(matches(isDisplayed()));
-        onView(withContentDescription("Navigate up")).check(matches(isDisplayed()));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_developerOptionsFragmentIntent() {
-        Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
-                .putExtra(EXTRA_FRAGMENT_ROOT_KEY, DEVELOPER_OPTIONS_KEY);
-        ActivityScenario.launch(intent);
-
-        onView(withText("Developer Options")).check(matches(isDisplayed()));
-        onView(withId(R.id.filter_box)).check(matches(isDisplayed()));
-        onView(withContentDescription("Navigate up")).check(matches(isDisplayed()));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_backButtonFinishesActivity() {
-        Bundle fragmentArgs = new Bundle();
-        fragmentArgs.putString(ARG_PREFERENCE_ROOT, "about_screen");
-        Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
-                .putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs);
-        ActivityScenario<SettingsActivity> scenario = ActivityScenario.launch(intent);
-
-        onView(withContentDescription("Navigate up")).perform(click());
-        scenario.onActivity(activity -> assertThat(activity.isFinishing()).isTrue());
-    }
-}
diff --git a/tests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java b/tests/multivalentTests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java
similarity index 100%
rename from tests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java
rename to tests/multivalentTests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/multivalentTests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
similarity index 95%
rename from tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
rename to tests/multivalentTests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 260f556..6cfa6ee 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -22,9 +22,9 @@
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyFloat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
@@ -41,6 +41,8 @@
 
 import com.android.launcher3.testcomponent.TouchEventGenerator;
 
+import com.google.errorprone.annotations.FormatMethod;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,7 +54,8 @@
 public class SingleAxisSwipeDetectorTest {
 
     private static final String TAG = SingleAxisSwipeDetectorTest.class.getSimpleName();
-    public static void L(String s, Object... parts) {
+    @FormatMethod
+    public static void logD(String s, Object... parts) {
         Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts));
     }
 
@@ -82,7 +85,7 @@
         mTouchSlop = orgConfig.getScaledTouchSlop();
         doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
 
-        L("mTouchSlop=", mTouchSlop);
+        logD("mTouchSlop= %s", mTouchSlop);
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index b83349e..b933ed2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -16,28 +16,42 @@
 
 package com.android.launcher3.ui;
 
+import static android.graphics.fonts.FontStyle.FONT_WEIGHT_BOLD;
+import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
+import static android.text.style.DynamicDrawableSpan.ALIGN_CENTER;
+
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
 import static com.android.launcher3.BubbleTextView.DISPLAY_ALL_APPS;
 import static com.android.launcher3.BubbleTextView.DISPLAY_PREDICTION_ROW;
 import static com.android.launcher3.BubbleTextView.DISPLAY_SEARCH_RESULT;
 import static com.android.launcher3.BubbleTextView.DISPLAY_SEARCH_RESULT_SMALL;
+import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
+import static com.android.launcher3.Flags.FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS;
 import static com.android.launcher3.LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Typeface;
+import android.os.Build;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.text.SpannedString;
+import android.text.style.ImageSpan;
 import android.view.ViewGroup;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.BubbleTextView;
@@ -403,6 +417,62 @@
         assertThat(mBubbleTextView.getIcon().hasBadge()).isEqualTo(false);
     }
 
+    @EnableFlags({FLAG_ENABLE_SUPPORT_FOR_ARCHIVING, FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS})
+    @Test
+    public void applyIconAndLabel_setsImageSpan_whenInactiveArchivedApp() {
+        // Given
+        BubbleTextView spyTextView = spy(mBubbleTextView);
+        mGmailAppInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+        BubbleTextView expectedTextView = new BubbleTextView(mContext);
+        mContext.getResources().getConfiguration().fontWeightAdjustment = 0;
+        int expectedDrawableId = mContext.getResources().getIdentifier(
+                "cloud_download_24px", /* name */
+                "drawable", /* defType */
+                mContext.getPackageName()
+        );
+        expectedTextView.setTextWithStartIcon(mGmailAppInfo.title, expectedDrawableId);
+        // When
+        spyTextView.applyIconAndLabel(mGmailAppInfo);
+        // Then
+        SpannedString expectedText = (SpannedString) expectedTextView.getText();
+        SpannedString actualText = (SpannedString) spyTextView.getText();
+        ImageSpan actualSpan = actualText.getSpans(
+                0, /* queryStart */
+                1, /* queryEnd */
+                ImageSpan.class
+        )[0];
+        ImageSpan expectedSpan = expectedText.getSpans(
+                0, /* queryStart */
+                1, /* queryEnd */
+                ImageSpan.class
+        )[0];
+        verify(spyTextView).setTextWithStartIcon(mGmailAppInfo.title, expectedDrawableId);
+        assertThat(actualText.toString()).isEqualTo(expectedText.toString());
+        assertThat(actualSpan.getDrawable().getBounds())
+                .isEqualTo(expectedSpan.getDrawable().getBounds());
+        assertThat(actualSpan.getVerticalAlignment()).isEqualTo(ALIGN_CENTER);
+    }
+
+    @EnableFlags({FLAG_ENABLE_SUPPORT_FOR_ARCHIVING, FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS})
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+    @Test
+    public void applyIconAndLabel_setsBoldDrawable_whenBoldedTextForArchivedApp() {
+        // Given
+        int expectedDrawableId = mContext.getResources().getIdentifier(
+                "cloud_download_semibold_24px", /* name */
+                "drawable", /* defType */
+                mContext.getPackageName()
+        );
+        mContext.getResources().getConfiguration().fontWeightAdjustment =
+                FONT_WEIGHT_BOLD - FONT_WEIGHT_NORMAL;
+        BubbleTextView spyTextView = spy(mBubbleTextView);
+        mGmailAppInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+        // When
+        spyTextView.applyIconAndLabel(mGmailAppInfo);
+        // Then
+        verify(spyTextView).setTextWithStartIcon(mGmailAppInfo.title, expectedDrawableId);
+    }
+
     @Test
     public void applyIconAndLabel_whenDisplay_DISPLAY_SEARCH_RESULT_hasBadge() {
         FlagOp op = FlagOp.NO_OP;
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
index 191d284..4217d22 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.util;
 
+import android.R;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.view.ContextThemeWrapper;
@@ -39,11 +40,16 @@
     private final MyDragLayer mMyDragLayer;
 
     public ActivityContextWrapper(Context base) {
-        super(base, android.R.style.Theme_DeviceDefault);
+        this(base, R.style.Theme_DeviceDefault);
+    }
+
+    public ActivityContextWrapper(Context base, int theme) {
+        super(base, theme);
         mProfile = InvariantDeviceProfile.INSTANCE.get(base).getDeviceProfile(base).copy(base);
         mMyDragLayer = new MyDragLayer(this);
     }
 
+
     @Override
     public BaseDragLayer getDragLayer() {
         return mMyDragLayer;
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 41effa2..a3a680e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -38,7 +38,10 @@
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.window.CachedDisplayInfo
 import com.android.launcher3.util.window.WindowManagerProxy
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
 import kotlin.math.min
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -79,7 +82,7 @@
             WindowBounds(Rect(0, 0, width, height), Rect(0, inset, 0, 0), Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, height, width), Rect(0, inset, 0, 0), Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, width, height), Rect(0, inset, 0, 0), Surface.ROTATION_180),
-            WindowBounds(Rect(0, 0, height, width), Rect(0, inset, 0, 0), Surface.ROTATION_270)
+            WindowBounds(Rect(0, 0, height, width), Rect(0, inset, 0, 0), Surface.ROTATION_270),
         )
     private val configuration =
         Configuration(appContext.resources.configuration).apply {
@@ -111,6 +114,7 @@
         whenever(windowManagerProxy.getRealBounds(any(), any())).thenAnswer { i ->
             bounds[i.getArgument<CachedDisplayInfo>(1).rotation]
         }
+        whenever(windowManagerProxy.showLockedTaskbarOnHome(any())).thenReturn(false)
 
         whenever(windowManagerProxy.getNavigationMode(any())).thenReturn(NavigationMode.NO_BUTTON)
         // Mock context
@@ -134,6 +138,13 @@
         displayController.addChangeListener(displayInfoChangeListener)
     }
 
+    @After
+    fun tearDown() {
+        // We need to reset the taskbar mode preference override even if a test throws an exception.
+        // Otherwise, it may break the following tests' assumptions.
+        DisplayController.enableTaskbarModePreferenceForTests(false)
+    }
+
     @Test
     @UiThreadTest
     fun testRotation() {
@@ -167,7 +178,7 @@
     @UiThreadTest
     fun testTaskbarPinning() {
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(true)
-        displayController.handleInfoChange(display)
+        displayController.notifyConfigChange()
         verify(displayInfoChangeListener)
             .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
     }
@@ -176,8 +187,41 @@
     @UiThreadTest
     fun testTaskbarPinningChangeInDesktopMode() {
         whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
-        displayController.handleInfoChange(display)
+        displayController.notifyConfigChange()
         verify(displayInfoChangeListener)
             .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
     }
+
+    @Test
+    @UiThreadTest
+    fun testTaskbarPinningChangeInLockedTaskbarChange() {
+        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.notifyConfigChange()
+        verify(displayInfoChangeListener)
+            .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/ItemInfoMatcherTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt
new file mode 100644
index 0000000..daba024
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.ComponentName
+import android.content.Intent
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ItemInfoMatcher.ofShortcutKeys
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ItemInfoMatcherTest {
+
+    @Test
+    fun `ofUser returns Predicate for ItemInfo containing given UserHandle`() {
+        val expectedItemInfo = ItemInfo().apply { user = UserHandle(11) }
+        val unexpectedItemInfo = ItemInfo().apply { user = UserHandle(0) }
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+        val predicate = ItemInfoMatcher.ofUser(UserHandle(11))
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofComponents returns Predicate for ItemInfo containing target Component and UserHandle`() {
+        // Given
+        val expectedUserHandle = UserHandle(0)
+        val expectedComponentName = ComponentName("expectedPackage", "expectedClass")
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.user = expectedUserHandle
+        whenever(expectedItemInfo.targetComponent).thenReturn(expectedComponentName)
+
+        val unexpectedComponentName = ComponentName("unexpectedPackage", "unexpectedClass")
+        val unexpectedItemInfo1 = spy(ItemInfo())
+        unexpectedItemInfo1.user = expectedUserHandle
+        whenever(unexpectedItemInfo1.targetComponent).thenReturn(unexpectedComponentName)
+
+        val unexpectedItemInfo2 = spy(ItemInfo())
+        unexpectedItemInfo2.user = UserHandle(10)
+        whenever(unexpectedItemInfo2.targetComponent).thenReturn(expectedComponentName)
+
+        val itemInfoStream =
+            listOf(expectedItemInfo, unexpectedItemInfo1, unexpectedItemInfo2).stream()
+
+        // When
+        val predicate =
+            ItemInfoMatcher.ofComponents(hashSetOf(expectedComponentName), expectedUserHandle)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofPackages returns Predicate for ItemInfo containing UserHandle and target package`() {
+        // Given
+        val expectedUserHandle = UserHandle(0)
+        val expectedPackage = "expectedPackage"
+        val expectedComponentName = ComponentName(expectedPackage, "expectedClass")
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.user = expectedUserHandle
+        whenever(expectedItemInfo.targetComponent).thenReturn(expectedComponentName)
+
+        val unexpectedPackage = "unexpectedPackage"
+        val unexpectedComponentName = ComponentName(unexpectedPackage, "unexpectedClass")
+        val unexpectedItemInfo1 = spy(ItemInfo())
+        unexpectedItemInfo1.user = expectedUserHandle
+        whenever(unexpectedItemInfo1.targetComponent).thenReturn(unexpectedComponentName)
+
+        val unexpectedItemInfo2 = spy(ItemInfo())
+        unexpectedItemInfo2.user = UserHandle(10)
+        whenever(unexpectedItemInfo2.targetComponent).thenReturn(expectedComponentName)
+
+        val itemInfoStream =
+            listOf(expectedItemInfo, unexpectedItemInfo1, unexpectedItemInfo2).stream()
+
+        // When
+        val predicate = ItemInfoMatcher.ofPackages(setOf(expectedPackage), expectedUserHandle)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofShortcutKeys returns Predicate for Deep Shortcut Info containing given ShortcutKey`() {
+        // Given
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        val expectedIntent =
+            Intent().apply {
+                putExtra("shortcut_id", "expectedShortcut")
+                `package` = "expectedPackage"
+            }
+        whenever(expectedItemInfo.intent).thenReturn(expectedIntent)
+
+        val unexpectedIntent =
+            Intent().apply {
+                putExtra("shortcut_id", "unexpectedShortcut")
+                `package` = "unexpectedPackage"
+            }
+        val unexpectedItemInfo = spy(ItemInfo())
+        unexpectedItemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        whenever(unexpectedItemInfo.intent).thenReturn(unexpectedIntent)
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+        val expectedShortcutKey = ShortcutKey.fromItemInfo(expectedItemInfo)
+
+        // When
+        val predicate = ItemInfoMatcher.ofShortcutKeys(setOf(expectedShortcutKey))
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `forFolderMatch returns Predicate to match against children within Folder ItemInfo`() {
+        // Given
+        val expectedItemInfo = spy(FolderInfo())
+        expectedItemInfo.itemType = ITEM_TYPE_FOLDER
+        val expectedIntent =
+            Intent().apply {
+                putExtra("shortcut_id", "expectedShortcut")
+                `package` = "expectedPackage"
+            }
+        val expectedChildInfo = spy(ItemInfo())
+        expectedChildInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        whenever(expectedChildInfo.intent).thenReturn(expectedIntent)
+        whenever(expectedItemInfo.getContents()).thenReturn(arrayListOf(expectedChildInfo))
+
+        val unexpectedItemInfo = spy(FolderInfo())
+        unexpectedItemInfo.itemType = ITEM_TYPE_FOLDER
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+        val expectedShortcutKey = ShortcutKey.fromItemInfo(expectedChildInfo)
+
+        // When
+        val predicate = ItemInfoMatcher.forFolderMatch(ofShortcutKeys(setOf(expectedShortcutKey)))
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofItemIds returns Predicate to match ItemInfo that contains given ids`() {
+        // Given
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.id = 1
+
+        val unexpectedItemInfo = spy(ItemInfo())
+        unexpectedItemInfo.id = 2
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+        // When
+        val expectedIds = IntSet().apply { add(1) }
+        val predicate = ItemInfoMatcher.ofItemIds(expectedIds)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofItems returns Predicate matching against provided ItemInfo`() {
+        // Given
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.id = 1
+
+        val unexpectedItemInfo = spy(ItemInfo())
+        unexpectedItemInfo.id = 2
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+        // When
+        val expectedItems = setOf(expectedItemInfo)
+        val predicate = ItemInfoMatcher.ofItems(expectedItems)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
index f18c02b..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;
@@ -45,21 +47,24 @@
 import android.util.ArrayMap;
 
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.uiautomator.UiDevice;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.testing.TestInformationProvider;
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
+import java.util.Arrays;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
@@ -85,6 +90,23 @@
     public static final String TEST_ACTIVITY13 = "com.android.launcher3.tests.Activity14";
     public static final String TEST_ACTIVITY14 = "com.android.launcher3.tests.Activity15";
 
+    public static final List<String> ACTIVITY_LIST = Arrays.asList(
+            TEST_ACTIVITY,
+            TEST_ACTIVITY2,
+            TEST_ACTIVITY3,
+            TEST_ACTIVITY4,
+            TEST_ACTIVITY5,
+            TEST_ACTIVITY6,
+            TEST_ACTIVITY7,
+            TEST_ACTIVITY8,
+            TEST_ACTIVITY9,
+            TEST_ACTIVITY10,
+            TEST_ACTIVITY11,
+            TEST_ACTIVITY12,
+            TEST_ACTIVITY13,
+            TEST_ACTIVITY14
+    );
+
     // Authority for providing a test default-workspace-layout data.
     private static final String TEST_PROVIDER_AUTHORITY =
             LauncherModelHelper.class.getName().toLowerCase();
@@ -128,7 +150,9 @@
         icon.eraseColor(Color.RED);
         sp.setAppIcon(icon);
         sp.setAppLabel(pkg);
-        PackageInstaller pi = sandboxContext.getPackageManager().getPackageInstaller();
+        sp.setInstallerPackageName(ApplicationProvider.getApplicationContext().getPackageName());
+        PackageInstaller pi = ApplicationProvider.getApplicationContext().getPackageManager()
+                .getPackageInstaller();
         int sessionId = pi.createSession(sp);
         mDestroyTask.add(() -> pi.abandonSession(sessionId));
         return sessionId;
@@ -163,12 +187,22 @@
      */
     public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
             throws Exception {
-        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext);
-        idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
-        idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
+        grantWriteSecurePermission();
 
-        UiDevice.getInstance(getInstrumentation()).executeShellCommand(
-                "settings put secure launcher3.layout.provider " + TEST_PROVIDER_AUTHORITY);
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext);
+        if (idp.numRows == 0 && idp.numColumns == 0) {
+            idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
+        }
+        if (idp.iconBitmapSize == 0) {
+            idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
+        }
+
+        Settings.Secure.putString(sandboxContext.getContentResolver(), "launcher3.layout.provider",
+                TEST_PROVIDER_AUTHORITY);
+
+        // TODO: use a wrapper class to differentiate the behavior
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        builder.build(new OutputStreamWriter(bos));
         ContentProvider cp = new TestInformationProvider() {
 
             @Override
@@ -177,8 +211,6 @@
                 try {
                     ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
                     AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]);
-                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
-                    builder.build(new OutputStreamWriter(bos));
                     outputStream.write(bos.toByteArray());
                     outputStream.flush();
                     outputStream.close();
@@ -189,9 +221,13 @@
             }
         };
         setupProvider(TEST_PROVIDER_AUTHORITY, cp);
+        RoboApiWrapper.INSTANCE.registerInputStream(sandboxContext.getContentResolver(),
+                ModelDbController.getLayoutUri(TEST_PROVIDER_AUTHORITY, sandboxContext),
+                ()-> new ByteArrayInputStream(bos.toByteArray()));
+
         mDestroyTask.add(() -> runOnExecutorSync(MODEL_EXECUTOR, () ->
-                UiDevice.getInstance(getInstrumentation()).executeShellCommand(
-                        "settings delete secure launcher3.layout.provider")));
+                Settings.Secure.putString(sandboxContext.getContentResolver(),
+                        "launcher3.layout.provider", "")));
         return this;
     }
 
@@ -203,7 +239,7 @@
         MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get();
 
         Executors.MODEL_EXECUTOR.submit(() -> { }).get();
-        MAIN_EXECUTOR.submit(() -> { }).get();
+        getInstrumentation().waitForIdleSync();
         MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get();
     }
 
@@ -215,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());
@@ -251,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/RunnableListTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
new file mode 100644
index 0000000..94bd7c9
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RunnableListTest {
+
+    @Mock private lateinit var runnable1: Runnable
+    @Mock private lateinit var runnable2: Runnable
+
+    private val underTest = RunnableList()
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun not_destroyedByDefault() {
+        assertThat(underTest.isDestroyed).isFalse()
+    }
+
+    @Test
+    fun add_and_run() {
+        underTest.add(runnable1)
+        underTest.add(runnable2)
+
+        underTest.executeAllAndDestroy()
+
+        verify(runnable1).run()
+        verify(runnable2).run()
+        assertThat(underTest.isDestroyed).isTrue()
+    }
+
+    @Test
+    fun add_to_destroyed_runnableList_run_immediately() {
+        underTest.executeAllAndDestroy()
+
+        underTest.add(runnable1)
+
+        verify(runnable1).run()
+    }
+
+    @Test
+    fun second_executeAllAndDestroy_noOp() {
+        underTest.executeAllAndDestroy()
+        underTest.add(runnable1)
+        reset(runnable1)
+
+        underTest.executeAllAndDestroy()
+
+        verifyNoMoreInteractions(runnable1)
+    }
+
+    @Test
+    fun executeAllAndClear_run_not_destroy() {
+        underTest.add(runnable1)
+        underTest.add(runnable2)
+
+        underTest.executeAllAndClear()
+
+        verify(runnable1).run()
+        verify(runnable2).run()
+        assertThat(underTest.isDestroyed).isFalse()
+    }
+
+    @Test
+    fun executeAllAndClear_not_destroy() {
+        underTest.executeAllAndClear()
+        underTest.add(runnable1)
+        reset(runnable1)
+
+        underTest.executeAllAndClear()
+
+        verify(runnable1).run()
+    }
+
+    @Test
+    fun remove_and_run_not_executed() {
+        underTest.add(runnable1)
+        underTest.add(runnable2)
+
+        underTest.remove(runnable1)
+        underTest.executeAllAndClear()
+
+        verifyNoMoreInteractions(runnable1)
+        verify(runnable2).run()
+    }
+}
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
new file mode 100644
index 0000000..45cc19c
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.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.util
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_SCREEN_OFF
+import android.content.Intent.ACTION_SCREEN_ON
+import android.content.Intent.ACTION_USER_PRESENT
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ScreenOnTrackerTest {
+
+    @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, tracker)
+    }
+
+    @Test
+    fun test_default_state() {
+        verify(receiver).register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT)
+        assertThat(underTest.isScreenOn).isTrue()
+    }
+
+    @Test
+    fun close_unregister_receiver() {
+        underTest.close()
+
+        verify(receiver).unregisterReceiverSafely(context)
+    }
+
+    @Test
+    fun add_listener_then_receive_screen_on_intent_notify_listener() {
+        underTest.addListener(listener)
+
+        underTest.onReceive(Intent(ACTION_SCREEN_ON))
+
+        verify(listener).onScreenOnChanged(true)
+        assertThat(underTest.isScreenOn).isTrue()
+    }
+
+    @Test
+    fun add_listener_then_receive_screen_off_intent_notify_listener() {
+        underTest.addListener(listener)
+
+        underTest.onReceive(Intent(ACTION_SCREEN_OFF))
+
+        verify(listener).onScreenOnChanged(false)
+        assertThat(underTest.isScreenOn).isFalse()
+    }
+
+    @Test
+    fun add_listener_then_receive_user_present_intent_notify_listener() {
+        underTest.addListener(listener)
+
+        underTest.onReceive(Intent(ACTION_USER_PRESENT))
+
+        verify(listener).onUserPresent()
+        assertThat(underTest.isScreenOn).isTrue()
+    }
+
+    @Test
+    fun remove_listener_then_receive_intent_noOp() {
+        underTest.addListener(listener)
+
+        underTest.removeListener(listener)
+
+        underTest.onReceive(Intent(ACTION_USER_PRESENT))
+        verifyNoMoreInteractions(listener)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
new file mode 100644
index 0000000..c43e563
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutUtilTest {
+
+    @Test
+    fun `supportsShortcuts returns true if the item is active and it is an app`() {
+        // A blank workspace item info should be active and should be ITEM_TYPE_APPLICATION
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.supportsShortcuts(itemInfo)
+        // Verify
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun `supportsDeepShortcuts returns true if the app is active and an app and widgets are enabled`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.supportsDeepShortcuts(itemInfo)
+        // Verify
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun `getShortcutIdIfPinnedShortcut returns null if the item is an app`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.getShortcutIdIfPinnedShortcut(itemInfo)
+        // Verify
+        assertNull(result)
+    }
+
+    @Test
+    fun `getPersonKeysIfPinnedShortcut returns empty string array if item type is an app`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.getPersonKeysIfPinnedShortcut(itemInfo)
+        // Verify
+        assertArrayEquals(Utilities.EMPTY_STRING_ARRAY, result)
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
similarity index 96%
rename from tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
index 1de99c5..d3e27b6 100644
--- a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
@@ -23,6 +23,7 @@
 import android.os.Looper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 import com.google.common.truth.Truth.assertThat
 import java.util.function.Consumer
@@ -114,6 +115,7 @@
         underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
 
         underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+        getInstrumentation().waitForIdleSync()
 
         verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
         verify(completionRunnable).run()
@@ -136,6 +138,7 @@
         underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
 
         underTest.unregisterReceiverSafely(context)
+        getInstrumentation().waitForIdleSync()
 
         verify(context).unregisterReceiver(same(underTest))
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
new file mode 100644
index 0000000..043bdac
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.view.View
+import android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.launcher3.util.SystemUiController.UI_STATE_BASE_WINDOW
+import com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SystemUiControllerTest {
+
+    @Mock private lateinit var window: Window
+    @Mock private lateinit var decorView: View
+
+    private lateinit var underTest: SystemUiController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        `when`(window.decorView).thenReturn(decorView)
+        underTest = SystemUiController(window.decorView)
+    }
+
+    @Test
+    fun test_default_state() {
+        assertThat(underTest.toString()).isEqualTo("mStates=[0, 0, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_base_window_light() {
+        underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+        val visibility =
+            View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+        verify(decorView).systemUiVisibility = eq(visibility)
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+        val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_scrim_view_light() {
+        underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ true)
+
+        verify(decorView).systemUiVisibility =
+            eq(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+        val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_base_window_dark() {
+        underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ false)
+
+        verify(decorView, never()).systemUiVisibility = anyInt()
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+        val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_scrim_view_dark() {
+        underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ false)
+
+        verify(decorView, never()).systemUiVisibility = anyInt()
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+        val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+    }
+
+    @Test
+    fun get_base_sysui_visibility() {
+        `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+    }
+
+    @Test
+    fun update_state_base_window_light_with_existing_visibility() {
+        `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+        underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+        val visibility =
+            View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or
+                View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
+                SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+        verify(decorView).systemUiVisibility = eq(visibility)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestDispatcherProvider.kt b/tests/multivalentTests/src/com/android/launcher3/util/TestDispatcherProvider.kt
new file mode 100644
index 0000000..39e1ec5
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestDispatcherProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * 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 com.android.launcher3.util.coroutines.DispatcherProvider
+import kotlinx.coroutines.CoroutineDispatcher
+
+class TestDispatcherProvider(testDispatcher: CoroutineDispatcher) : DispatcherProvider {
+    override val default: CoroutineDispatcher = testDispatcher
+    override val io: CoroutineDispatcher = testDispatcher
+    override val main: CoroutineDispatcher = testDispatcher
+    override val unconfined: CoroutineDispatcher = testDispatcher
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
index 3f37563..71637f1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
@@ -57,6 +58,8 @@
     protected ActivityAllAppsContainerView<ActivityContextWrapper> mAppsView;
 
     private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+    private final WidgetPickerDataProvider mWidgetPickerDataProvider =
+            new WidgetPickerDataProvider();
     protected final UserCache mUserCache;
 
     public TestSandboxModelContextWrapper(SandboxContext base) {
@@ -76,12 +79,19 @@
         mAppsList = mAppsView.getPersonalAppList();
         mAllAppsStore = mAppsView.getAppsStore();
     }
+
     @Nullable
     @Override
     public PopupDataProvider getPopupDataProvider() {
         return mPopupDataProvider;
     }
 
+    @Nullable
+    @Override
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
+    }
+
     @Override
     public ActivityAllAppsContainerView<ActivityContextWrapper> getAppsView() {
         return mAppsView;
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
new file mode 100644
index 0000000..0f212eb
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.media.AudioAttributes
+import android.os.VibrationEffect
+import android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK
+import android.os.VibrationEffect.Composition.PRIMITIVE_TICK
+import android.os.Vibrator
+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
+import com.google.common.truth.Truth.assertThat
+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.Mockito.any
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+class VibratorWrapperTest {
+
+    @Mock private lateinit var settingsCache: SettingsCache
+    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(context, settingsCache, tracker)
+    }
+
+    @Test
+    fun init_register_onChangeListener() {
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
+        verify(settingsCache).register(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+    }
+
+    @Test
+    fun vibrate() {
+        underTest.vibrate(OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+    }
+
+    @Test
+    fun vibrate_primitive_id() {
+        underTest.vibrate(PRIMITIVE_TICK, 1f, OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 1f).compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_with_invalid_primitive_id_use_fallback_effect() {
+        underTest.vibrate(-1, 1f, OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+    }
+
+    @Test
+    fun vibrate_for_taskbar_unstash() {
+        underTest.vibrateForTaskbarUnstash()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.LOW_TICK_SCALE)
+                .compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun haptic_feedback_disabled_no_vibrate() {
+        `when`(vibrator.hasVibrator()).thenReturn(false)
+        underTest = VibratorWrapper(context, settingsCache, tracker)
+
+        underTest.vibrate(OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator, never())
+            .vibrate(any(VibrationEffect::class.java), any(AudioAttributes::class.java))
+    }
+
+    @Test
+    fun cancel_vibrate() {
+        underTest.cancelVibrate()
+
+        awaitTasksCompleted()
+        verify(vibrator).cancel()
+    }
+
+    private fun awaitTasksCompleted() {
+        Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
new file mode 100644
index 0000000..d82818d
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.R
+import com.android.launcher3.util.ViewCache.CacheEntry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ViewCacheTest {
+
+    private lateinit var underTest: ViewCache
+
+    private val context = InstrumentationRegistry.getInstrumentation().context
+    private val layoutId =
+        context.run { resources.getIdentifier("test_layout_appwidget_blue", "layout", packageName) }
+
+    @Before
+    fun setUp() {
+        underTest = ViewCache()
+        underTest.setCacheSize(layoutId, 5)
+    }
+
+    @Test
+    fun get_view_from_empty_cache() {
+        val view: View = underTest.getView(layoutId, context, null)
+
+        val cacheEntry = view.getTag(R.id.cache_entry_tag_id) as ViewCache.CacheEntry
+        assertThat(cacheEntry).isNotNull()
+        assertThat(cacheEntry.mMaxSize).isEqualTo(5)
+        assertThat(cacheEntry.mCurrentSize).isEqualTo(0)
+        assertThat(cacheEntry.mViews[0]).isNull()
+    }
+
+    @Test
+    fun recyclerView() {
+        val view: View = underTest.getView(layoutId, context, null)
+        val cacheEntry = view.getTag(R.id.cache_entry_tag_id) as ViewCache.CacheEntry
+
+        underTest.recycleView(layoutId, view)
+
+        assertThat(cacheEntry.mMaxSize).isEqualTo(5)
+        assertThat(cacheEntry.mCurrentSize).isEqualTo(1)
+        assertThat(cacheEntry.mViews[0]).isSameInstanceAs(view)
+    }
+
+    @Test
+    fun get_view_from_cache() {
+        val view: View = underTest.getView(layoutId, context, null)
+        underTest.recycleView(layoutId, view)
+
+        val newView = underTest.getView<View>(layoutId, context, null)
+
+        assertThat(view).isSameInstanceAs(newView)
+    }
+
+    @Test
+    fun change_tag_id_recyclerView_noOp() {
+        val view: View = underTest.getView(layoutId, context, null)
+        val cacheEntry = view.getTag(R.id.cache_entry_tag_id) as ViewCache.CacheEntry
+
+        view.setTag(R.id.cache_entry_tag_id, CacheEntry(3))
+        underTest.recycleView(layoutId, view)
+
+        assertThat(cacheEntry.mMaxSize).isEqualTo(5)
+        assertThat(cacheEntry.mCurrentSize).isEqualTo(0)
+        assertThat(cacheEntry.mViews[0]).isNull()
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewOnDrawExecutorTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewOnDrawExecutorTest.kt
new file mode 100644
index 0000000..d26c4d4
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewOnDrawExecutorTest.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Launcher
+import com.android.launcher3.Workspace
+import com.android.launcher3.pageindicators.PageIndicator
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@RunWith(AndroidJUnit4::class)
+class ViewOnDrawExecutorTest<T> where T : View, T : PageIndicator {
+
+    @Mock private lateinit var runnable: Runnable
+    @Mock private lateinit var consumer: Consumer<ViewOnDrawExecutor>
+    @Mock private lateinit var launcher: Launcher
+    @Mock private lateinit var workspace: Workspace<T>
+    @Mock private lateinit var rootView: View
+    @Mock private lateinit var viewTreeObserver: ViewTreeObserver
+
+    private lateinit var underTest: ViewOnDrawExecutor
+    private lateinit var runnableList: RunnableList
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        runnableList = RunnableList()
+        runnableList.add(runnable)
+        underTest = ViewOnDrawExecutor(runnableList, consumer)
+
+        `when`(launcher.workspace).thenReturn(workspace)
+        `when`(workspace.rootView).thenReturn(rootView)
+        `when`(workspace.viewTreeObserver).thenReturn(viewTreeObserver)
+    }
+
+    @Test
+    fun attachToLauncher_alreadyAttachedToWindow() {
+        `when`(workspace.isAttachedToWindow).thenReturn(true)
+
+        underTest.attachTo(launcher)
+
+        verify(workspace).addOnAttachStateChangeListener(same(underTest))
+        verify(viewTreeObserver).addOnDrawListener(same(underTest))
+        verify(rootView).invalidate()
+    }
+
+    @Test
+    fun attachToLauncher_notAttachedToWindow() {
+        `when`(workspace.isAttachedToWindow).thenReturn(false)
+
+        underTest.attachTo(launcher)
+
+        verify(workspace).addOnAttachStateChangeListener(same(underTest))
+        verifyNoMoreInteractions(viewTreeObserver)
+        verifyNoMoreInteractions(rootView)
+    }
+
+    @Test
+    fun onViewAttachedToWindow_registerObserver() {
+        `when`(workspace.isAttachedToWindow).thenReturn(false)
+        underTest.attachTo(launcher)
+
+        underTest.onViewAttachedToWindow(rootView)
+
+        verify(viewTreeObserver).addOnDrawListener(same(underTest))
+        verify(rootView).invalidate()
+    }
+
+    @Test
+    fun complete_then_onViewAttachedToWindow_registerObserver() {
+        underTest.markCompleted()
+        reset(viewTreeObserver)
+        reset(rootView)
+
+        underTest.onViewAttachedToWindow(rootView)
+
+        verifyNoMoreInteractions(viewTreeObserver)
+        verifyNoMoreInteractions(rootView)
+    }
+
+    @Test
+    fun onDraw_postRunnable() {
+        underTest.attachTo(launcher)
+
+        underTest.onDraw()
+
+        verify(workspace).post(same(underTest))
+    }
+
+    @Test
+    fun run_before_onDraw_noOp() {
+        underTest.run()
+
+        verifyNoMoreInteractions(runnable)
+        verifyNoMoreInteractions(viewTreeObserver)
+        verifyNoMoreInteractions(workspace)
+        verifyNoMoreInteractions(consumer)
+    }
+
+    @Test
+    fun first_run_executeRunnable() {
+        underTest.attachTo(launcher)
+        underTest.onDraw()
+
+        underTest.run()
+
+        verify(runnable).run()
+        verify(viewTreeObserver).removeOnDrawListener(same(underTest))
+        verify(workspace).removeOnAttachStateChangeListener(same(underTest))
+        verify(consumer).accept(same(underTest))
+    }
+
+    @Test
+    fun second_run_noOp() {
+        underTest.attachTo(launcher)
+        underTest.onDraw()
+        underTest.run()
+        reset(runnable)
+        reset(viewTreeObserver)
+        reset(workspace)
+        reset(consumer)
+
+        underTest.run()
+
+        verifyNoMoreInteractions(runnable)
+        verifyNoMoreInteractions(viewTreeObserver)
+        verifyNoMoreInteractions(workspace)
+        verifyNoMoreInteractions(consumer)
+    }
+
+    @Test
+    fun markCompleted_viewNotAttached() {
+        underTest.markCompleted()
+
+        verify(runnable).run()
+        verify(consumer).accept(underTest)
+        verifyNoMoreInteractions(workspace)
+    }
+
+    @Test
+    fun markCompleted_viewAttached() {
+        underTest.attachTo(launcher)
+
+        underTest.markCompleted()
+
+        verify(runnable).run()
+        verify(consumer).accept(underTest)
+        verify(workspace).removeOnAttachStateChangeListener(same(underTest))
+        verify(viewTreeObserver).removeOnDrawListener(same(underTest))
+    }
+
+    @Test
+    fun cancel_notRun() {
+        underTest.cancel()
+
+        verifyNoMoreInteractions(runnable)
+        verify(consumer).accept(underTest)
+        verifyNoMoreInteractions(workspace)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
new file mode 100644
index 0000000..e535656
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.same
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+@UiThreadTest
+class ViewPoolTest {
+
+    @Mock private lateinit var viewParent: ViewGroup
+    @Mock private lateinit var view: ReusableView
+    @Mock private lateinit var inflater: LayoutInflater
+
+    private lateinit var underTest: ViewPool<ReusableView>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        `when`(inflater.cloneInContext(any())).thenReturn(inflater)
+        underTest = ViewPool(inflater, viewParent, LAYOUT_ID, 10, 0)
+    }
+
+    @Test
+    fun get_view_from_empty_pool_inflate_new_view() {
+        underTest.view
+
+        verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+    }
+
+    @Test
+    fun recycle_view() {
+        underTest.recycle(view)
+
+        val returnedView = underTest.view
+
+        verify(view).onRecycle()
+        assertThat(returnedView).isSameInstanceAs(view)
+        verifyNoMoreInteractions(inflater)
+    }
+
+    @Test
+    fun get_view_twice_from_view_pool_with_one_view() {
+        underTest.recycle(view)
+        underTest.view
+        verifyNoMoreInteractions(inflater)
+
+        underTest.view
+
+        verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+    }
+
+    companion object {
+        private const val LAYOUT_ID = 1000
+    }
+
+    private inner class ReusableView(context: Context) : View(context), ViewPool.Reusable {
+        override fun onRecycle() {
+            TODO("Not yet implemented")
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index 909aabd..6313cf0 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -47,7 +47,7 @@
                     + ")$");
     private static final Pattern PLATFORM_BUILD =
             Pattern.compile("^("
-                    + "(?<commandLine>eng\\.[a-z]+\\.[0-9]+\\.[0-9]+)|"
+                    + "(?<commandLine>eng\\..+)|"
                     + "(?<presubmit>P[0-9]+)|"
                     + "(?<postsubmit>[0-9]+)"
                     + ")$");
@@ -109,6 +109,9 @@
                     getPackageManager().
                     getPackageInfo(launcherPackageName, 0)
                     .versionName;
+            if (launcherVersion == null) {
+                return LOCAL;
+            }
         } catch (PackageManager.NameNotFoundException e) {
             throw new RuntimeException(e);
         }
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
index ec83b8b..7484bce 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -38,8 +38,8 @@
     @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
     private val providerName =
         ComponentName(
-            "com.android.launcher3.tests",
-            "com.android.launcher3.testcomponent.AppWidgetNoConfig"
+            getInstrumentation().getContext().getPackageName(),
+            "com.android.launcher3.testcomponent.AppWidgetNoConfig",
         )
     private val generatedPreviewLayout =
         getInstrumentation().context.run {
@@ -61,7 +61,7 @@
                     ActivityContextWrapper(
                         ContextThemeWrapper(
                             context,
-                            com.android.launcher3.R.style.WidgetContainerTheme
+                            com.android.launcher3.R.style.WidgetContainerTheme,
                         )
                     )
                 )
@@ -78,7 +78,7 @@
             object : WidgetManagerHelper(context) {
                 override fun loadGeneratedPreview(
                     info: AppWidgetProviderInfo,
-                    widgetCategory: Int
+                    widgetCategory: Int,
                 ) =
                     generatedPreview.takeIf {
                         info === appWidgetProviderInfo &&
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt
new file mode 100644
index 0000000..79b493a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.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.widget
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.Executors
+import java.util.function.IntConsumer
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LauncherAppWidgetHostTest {
+
+    @Mock private lateinit var onAppWidgetRemovedCallback: IntConsumer
+
+    private val context = ActivityContextWrapper(getInstrumentation().targetContext)
+    private lateinit var underTest: LauncherAppWidgetHost
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = LauncherAppWidgetHost(context, onAppWidgetRemovedCallback, emptyList())
+    }
+
+    @Test
+    fun `Host set view to recycle`() {
+        val mockRecycleView = mock(ListenableHostView::class.java)
+
+        assertNull(underTest.viewToRecycle)
+        underTest.recycleViewForNextCreation(mockRecycleView)
+
+        assertSame(mockRecycleView, underTest.viewToRecycle)
+    }
+
+    @Test
+    fun `Host create view`() {
+        val mockRecycleView = mock(ListenableHostView::class.java)
+
+        var resultView = underTest.onCreateView(context, WIDGET_ID, null)
+
+        assertNotSame(mockRecycleView, resultView)
+
+        underTest.recycleViewForNextCreation(mockRecycleView)
+        resultView = underTest.onCreateView(context, WIDGET_ID, null)
+
+        assertSame(mockRecycleView, resultView)
+    }
+
+    @Test
+    fun `Runnable called when app widget removed`() {
+        underTest.onAppWidgetRemoved(WIDGET_ID)
+
+        Executors.MODEL_EXECUTOR.submit {}.get()
+        getInstrumentation().waitForIdleSync()
+
+        verify(onAppWidgetRemovedCallback).accept(WIDGET_ID)
+    }
+
+    companion object {
+        const val WIDGET_ID = 10001
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
new file mode 100644
index 0000000..1a659e2
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget
+
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.BuildConfig.WIDGETS_ENABLED
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_ACTIVITY_RESUMED
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_ACTIVITY_STARTED
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_STATE_IS_NORMAL
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+class LauncherWidgetHolderTest {
+    private lateinit var widgetHolder: LauncherWidgetHolder
+
+    @Before
+    fun setUp() {
+        assertTrue(WIDGETS_ENABLED)
+        widgetHolder =
+            LauncherWidgetHolder(ActivityContextWrapper(getInstrumentation().targetContext)) {}
+    }
+
+    @After
+    fun tearDown() {
+        widgetHolder.destroy()
+    }
+
+    @Test
+    fun widget_holder_start_listening() {
+        val testView = mock(PendingAppWidgetHostView::class.java)
+        widgetHolder.mViews[0] = testView
+        widgetHolder.setListeningFlag(false)
+        assertFalse(widgetHolder.isListening)
+        widgetHolder.startListening()
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        getInstrumentation().waitForIdleSync()
+        assertTrue(widgetHolder.isListening)
+        verify(testView, times(1)).reInflate()
+        widgetHolder.clearWidgetViews()
+    }
+
+    @Test
+    fun holder_start_listening_after_activity_start() {
+        widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_RESUMED, true)
+        widgetHolder.setActivityStarted(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setActivityStarted(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    fun holder_start_listening_after_activity_resume() {
+        widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_STARTED, true)
+        widgetHolder.setActivityResumed(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setActivityResumed(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    fun holder_start_listening_after_state_normal() {
+        widgetHolder.setShouldListenFlag(FLAG_ACTIVITY_RESUMED or FLAG_ACTIVITY_STARTED, true)
+        widgetHolder.setStateIsNormal(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setStateIsNormal(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    @UiThreadTest
+    fun widget_holder_create_view() {
+        val mockProviderInfo = mock(LauncherAppWidgetProviderInfo::class.java)
+        doReturn(false).whenever(mockProviderInfo).isCustomWidget
+        assertEquals(0, widgetHolder.mViews.size())
+        widgetHolder.createView(APP_WIDGET_ID, mockProviderInfo)
+        assertEquals(1, widgetHolder.mViews.size())
+        assertEquals(APP_WIDGET_ID, widgetHolder.mViews.get(0).appWidgetId)
+        widgetHolder.deleteAppWidgetId(APP_WIDGET_ID)
+        assertEquals(0, widgetHolder.mViews.size())
+    }
+
+    @Test
+    fun holder_add_provider_change_listener() {
+        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        widgetHolder.addProviderChangeListener(listener)
+        getInstrumentation().waitForIdleSync()
+        assertEquals(1, widgetHolder.mProviderChangedListeners.size)
+        assertSame(widgetHolder.mProviderChangedListeners.first(), listener)
+        widgetHolder.removeProviderChangeListener(listener)
+    }
+
+    @Test
+    fun holder_remove_provider_change_listener() {
+        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        widgetHolder.addProviderChangeListener(listener)
+        widgetHolder.removeProviderChangeListener(listener)
+        getInstrumentation().waitForIdleSync()
+        assertEquals(0, widgetHolder.mProviderChangedListeners.size)
+    }
+
+    @Test
+    fun widget_holder_stop_listening() {
+        widgetHolder.setListeningFlag(true)
+        assertTrue(widgetHolder.isListening)
+        widgetHolder.stopListening()
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.isListening)
+    }
+
+    companion object {
+        private const val APP_WIDGET_ID = 0
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/ListenableHostViewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/ListenableHostViewTest.kt
new file mode 100644
index 0000000..6c71f36
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/ListenableHostViewTest.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.widget
+
+import android.content.Context
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ListenableHostViewTest {
+
+    private val context: Context
+        get() = ActivityContextWrapper(InstrumentationRegistry.getInstrumentation().targetContext)
+
+    @Test
+    fun updateAppWidget_notifiesListeners() {
+        val hostView = ListenableHostView(context)
+        var wasNotifiedOfUpdate = false
+        val updateListener = Runnable { wasNotifiedOfUpdate = true }
+        hostView.addUpdateListener(updateListener)
+        hostView.beginDeferringUpdates()
+        hostView.updateAppWidget(null)
+        Truth.assertThat(wasNotifiedOfUpdate).isTrue()
+    }
+
+    @Test
+    fun onInitializeAccessibilityNodeInfo_correctlySetsClassName() {
+        val hostView = ListenableHostView(context)
+        val nodeInfo = AccessibilityNodeInfo()
+        hostView.onInitializeAccessibilityNodeInfo(nodeInfo)
+        Truth.assertThat(nodeInfo.className).isEqualTo(LauncherAppWidgetHostView::class.java.name)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
new file mode 100644
index 0000000..c82e84c
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import 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
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RoundedCornerEnforcementTest {
+
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+    @Test
+    fun `Widget view has one background`() {
+        val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+
+        doReturn(android.R.id.background).whenever(mockWidgetView).id
+
+        assertSame(RoundedCornerEnforcement.findBackground(mockWidgetView), mockWidgetView)
+    }
+
+    @Test
+    fun `Widget opted out of rounded corner enforcement`() {
+        val mockView = mock(View::class.java)
+
+        doReturn(android.R.id.background).whenever(mockView).id
+        doReturn(true).whenever(mockView).clipToOutline
+
+        assertTrue(RoundedCornerEnforcement.hasAppWidgetOptedOut(mockView))
+    }
+
+    @Test
+    fun `Compute rect based on widget view with background`() {
+        val mockBackgroundView = mock(View::class.java)
+        val mockWidgetView = mock(ViewGroup::class.java)
+        val testRect = Rect(0, 0, 0, 0)
+
+        doReturn(WIDTH).whenever(mockBackgroundView).width
+        doReturn(HEIGHT).whenever(mockBackgroundView).height
+        doReturn(LEFT).whenever(mockBackgroundView).left
+        doReturn(TOP).whenever(mockBackgroundView).top
+        doReturn(mockWidgetView).whenever(mockBackgroundView).parent
+
+        RoundedCornerEnforcement.computeRoundedRectangle(
+            mockWidgetView,
+            mockBackgroundView,
+            testRect,
+        )
+
+        assertEquals(Rect(50, 75, 250, 275), testRect)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+    fun `Compute system radius when smaller`() {
+        val mockContext = mock(Context::class.java)
+        val mockRes = mock(Resources::class.java)
+
+        doReturn(mockRes).whenever(mockContext).resources
+        doReturn(RADIUS)
+            .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(RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+    fun `Compute launcher radius when smaller`() {
+        val mockContext = mock(Context::class.java)
+        val mockRes = mock(Resources::class.java)
+
+        doReturn(mockRes).whenever(mockContext).resources
+        doReturn(LAUNCHER_RADIUS + 8f)
+            .whenever(mockRes)
+            .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+        doReturn(LAUNCHER_RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+        assertEquals(LAUNCHER_RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+    fun `Compute system radius ignoring launcher radius`() {
+        val mockContext = mock(Context::class.java)
+        val mockRes = mock(Resources::class.java)
+
+        doReturn(mockRes).whenever(mockContext).resources
+        val systemRadius = LAUNCHER_RADIUS + 8f
+        doReturn(systemRadius)
+            .whenever(mockRes)
+            .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+        doReturn(LAUNCHER_RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+        assertEquals(systemRadius, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+    }
+
+    companion object {
+        const val WIDTH = 200
+        const val HEIGHT = 200
+        const val LEFT = 50
+        const val TOP = 75
+
+        const val RADIUS = 8f
+        const val LAUNCHER_RADIUS = 16f
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/WidgetAddFlowHandlerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetAddFlowHandlerTest.kt
new file mode 100644
index 0000000..242ec7c
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetAddFlowHandlerTest.kt
@@ -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.launcher3.widget
+
+import android.content.Context
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.Launcher
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetAddFlowHandlerTest {
+
+    private val context: Context
+        get() = ActivityContextWrapper(InstrumentationRegistry.getInstrumentation().targetContext)
+
+    private val providerInfo =
+        LauncherAppWidgetProviderInfo().apply {
+            configure = InstrumentationRegistry.getInstrumentation().componentName
+        }
+    private val appWidgetHolder: LauncherWidgetHolder = mock<LauncherWidgetHolder>()
+    private val launcher: Launcher =
+        mock<Launcher>().also { whenever(it.appWidgetHolder).thenReturn(appWidgetHolder) }
+    private val appWidgetInfo = LauncherAppWidgetInfo().apply { appWidgetId = 123 }
+    private val requestCode = 123
+    private val flowHandler = WidgetAddFlowHandler(providerInfo)
+
+    @Test
+    fun valuesShouldRemainTheSame_beforeAndAfter_parcelization() {
+        with(Bundle()) {
+            val testKey = "testKey"
+            putParcelable(testKey, flowHandler)
+            Truth.assertThat(getParcelable(testKey, WidgetAddFlowHandler::class.java))
+                .isEqualTo(flowHandler)
+        }
+    }
+
+    @Test
+    fun describeContents_shouldReturn_0() {
+        Truth.assertThat(flowHandler.describeContents()).isEqualTo(0)
+    }
+
+    @Test
+    fun startBindFlow_shouldCorrectly_startLauncherFlowBinding() {
+        flowHandler.startBindFlow(launcher, appWidgetInfo.appWidgetId, appWidgetInfo, requestCode)
+        verify(launcher).setWaitingForResult(any())
+        verify(appWidgetHolder)
+            .startBindFlow(launcher, appWidgetInfo.appWidgetId, providerInfo, requestCode)
+    }
+
+    @Test
+    fun startConfigActivityWithCustomAppWidgetId_shouldAskLauncherToStartConfigActivity() {
+        flowHandler.startConfigActivity(
+            launcher,
+            appWidgetInfo.appWidgetId,
+            ItemInfo(),
+            requestCode
+        )
+        verify(launcher).setWaitingForResult(any())
+        verify(appWidgetHolder)
+            .startConfigActivity(launcher, appWidgetInfo.appWidgetId, requestCode)
+    }
+
+    @Test
+    fun startConfigActivity_shouldAskLauncherToStartConfigActivity() {
+        flowHandler.startConfigActivity(launcher, appWidgetInfo, requestCode)
+        verify(launcher).setWaitingForResult(any())
+        verify(appWidgetHolder)
+            .startConfigActivity(launcher, appWidgetInfo.appWidgetId, requestCode)
+    }
+
+    @Test
+    fun needsConfigure_returnsTrue_ifFlagsAndProviderInfoDetermineSo() {
+        Truth.assertThat(flowHandler.needsConfigure()).isTrue()
+    }
+
+    @Test
+    fun getProviderInfo_returnCorrectProviderInfo() {
+        Truth.assertThat(flowHandler.getProviderInfo(context)).isSameInstanceAs(providerInfo)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/WidgetManagerHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetManagerHelperTest.kt
new file mode 100644
index 0000000..f1cfb79
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetManagerHelperTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.os.Bundle
+import android.os.Process
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.PackageUserKey
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetManagerHelperTest {
+
+    private val context: Context
+        get() = ActivityContextWrapper(InstrumentationRegistry.getInstrumentation().targetContext)
+
+    private val info =
+        LauncherAppWidgetProviderInfo().apply {
+            provider = InstrumentationRegistry.getInstrumentation().componentName
+            providerInfo =
+                mock(ActivityInfo::class.java).apply { applicationInfo = context.applicationInfo }
+        }
+
+    @Mock private lateinit var appWidgetManager: AppWidgetManager
+
+    private lateinit var underTest: WidgetManagerHelper
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = WidgetManagerHelper(context, appWidgetManager)
+    }
+
+    @Test
+    fun getAllProviders_returnsCorrectWidgetProviderInfo() {
+        val packageUserKey =
+            mock(PackageUserKey::class.java).apply {
+                mPackageName = context.packageName
+                mUser = Process.myUserHandle()
+            }
+        val desiredResult = listOf(info)
+        whenever(
+                appWidgetManager.getInstalledProvidersForPackage(
+                    packageUserKey.mPackageName,
+                    packageUserKey.mUser
+                )
+            )
+            .thenReturn(desiredResult)
+        Truth.assertThat(underTest.getAllProviders(packageUserKey)).isSameInstanceAs(desiredResult)
+    }
+
+    @Test
+    fun getLauncherAppWidgetInfo_returnsCorrectInfo_ifWidgetExists() {
+        val id = 123
+        whenever(appWidgetManager.getAppWidgetInfo(id)).thenReturn(info)
+        val componentName = InstrumentationRegistry.getInstrumentation().componentName
+        Truth.assertThat(underTest.getLauncherAppWidgetInfo(id, componentName))
+            .isSameInstanceAs(info)
+    }
+
+    @Test
+    fun bindAppWidgetIdIfAllowed_correctly_forwardsBindCommandToAppWidgetManager() {
+        val id = 124
+        val options = Bundle()
+        underTest.bindAppWidgetIdIfAllowed(id, info, options)
+        verify(appWidgetManager).bindAppWidgetIdIfAllowed(id, info.profile, info.provider, options)
+    }
+
+    @Test
+    fun findProvider_returnsNull_ifNoProviderExists() {
+        val info =
+            underTest.getLauncherAppWidgetInfo(
+                1,
+                InstrumentationRegistry.getInstrumentation().componentName
+            )
+        Truth.assertThat(info).isNull()
+    }
+
+    @Test
+    fun isAppWidgetRestored_returnsTrue_ifWidgetIsRestored() {
+        val id = 126
+        whenever(appWidgetManager.getAppWidgetOptions(id))
+            .thenReturn(
+                Bundle().apply {
+                    putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true)
+                }
+            )
+        Truth.assertThat(underTest.isAppWidgetRestored(id)).isTrue()
+    }
+
+    @Test
+    fun loadGeneratedPreview_returnsWidgetPreview_fromAppWidgetManager() {
+        val widgetCategory = 130
+        with(info) {
+            underTest.loadGeneratedPreview(this, widgetCategory)
+            verify(appWidgetManager).getWidgetPreview(provider, profile, widgetCategory)
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
new file mode 100644
index 0000000..af2c378
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.custom
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CustomAppWidgetProviderInfoTest {
+
+    private lateinit var underTest: CustomAppWidgetProviderInfo
+
+    @Before
+    fun setup() {
+        underTest = CustomAppWidgetProviderInfo()
+        underTest.provider = PROVIDER_NAME
+    }
+
+    @Test
+    fun info_to_string() {
+        assertEquals("WidgetProviderInfo($PROVIDER_NAME)", underTest.toString())
+    }
+
+    @Test
+    fun get_label() {
+        underTest.label = "  TEST_LABEL"
+        assertEquals(LABEL_NAME, underTest.getLabel())
+    }
+
+    companion object {
+        private val PROVIDER_NAME =
+            ComponentName(getInstrumentation().targetContext.packageName, "TEST_PACKAGE")
+        private const val LABEL_NAME = "TEST_LABEL"
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
new file mode 100644
index 0000000..1c25db9
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.custom
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.os.Process
+import android.platform.test.flag.junit.SetFlagsRule
+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.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
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomWidgetManagerTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val context = SandboxModelContext()
+    private lateinit var underTest: CustomWidgetManager
+
+    @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)
+        underTest = CustomWidgetManager(context, pluginManager, mockAppWidgetManager, tracker)
+    }
+
+    @Test
+    fun plugin_manager_added_after_initialization() {
+        verify(pluginManager)
+            .addPluginListener(same(underTest), same(CustomWidgetPlugin::class.java), eq(true))
+    }
+
+    @Test
+    fun close_widget_manager_should_remove_plugin_listener() {
+        verify(tracker).addCloseable(closableCaptor.capture())
+        closableCaptor.allValues.forEach(SafeCloseable::close)
+        verify(pluginManager).removePluginListener(same(underTest))
+    }
+
+    @Test
+    fun on_plugin_connected_no_provider_info() {
+        doReturn(emptyList<LauncherAppWidgetProviderInfo>())
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(any())
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        assertEquals(0, underTest.plugins.size)
+    }
+
+    @Test
+    fun on_plugin_connected_exist_provider_info() {
+        doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        assertEquals(1, underTest.plugins.size)
+    }
+
+    @Test
+    fun on_plugin_disconnected() {
+        doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        underTest.onPluginDisconnected(mockPlugin)
+        assertEquals(0, underTest.plugins.size)
+    }
+
+    @Test
+    fun on_view_created() {
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+        val mockProviderInfo = mock(CustomAppWidgetProviderInfo::class.java)
+        doReturn(mockProviderInfo).whenever(mockWidgetView).appWidgetInfo
+        mockProviderInfo.provider = TEST_COMPONENT_NAME
+        underTest.plugins.put(TEST_COMPONENT_NAME, mockPlugin)
+        underTest.onViewCreated(mockWidgetView)
+        verify(mockPlugin).onViewCreated(eq(mockWidgetView))
+    }
+
+    @Test
+    fun generate_stream() {
+        assertTrue(underTest.stream().toList().isEmpty())
+        doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        assertEquals(1, underTest.stream().toList().size)
+    }
+
+    companion object {
+        private const val TEST_CLASS = "TEST_CLASS"
+        private val TEST_COMPONENT_NAME =
+            ComponentName(getInstrumentation().targetContext.packageName, TEST_CLASS)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
new file mode 100644
index 0000000..063ab32
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.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.widget.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+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
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+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.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetsListBaseEntriesBuilderTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var iconCache: IconCache
+
+    private lateinit var userHandle: UserHandle
+    private lateinit var context: Context
+    private lateinit var testInvariantProfile: InvariantDeviceProfile
+    private lateinit var allWidgets: Map<PackageItemInfo, List<WidgetItem>>
+    private lateinit var underTest: WidgetsListBaseEntriesBuilder
+
+    @Before
+    fun setUp() {
+        userHandle = UserHandle.CURRENT
+        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        testInvariantProfile = LauncherAppState.getIDP(context)
+
+        doAnswer { invocation: InvocationOnMock ->
+                val componentWithLabel = invocation.getArgument<Any>(0) as CachedObject
+                componentWithLabel.getComponent().shortClassName
+            }
+            .`when`(iconCache)
+            .getTitleNoCache(any<CachedObject>())
+        underTest = WidgetsListBaseEntriesBuilder(context)
+
+        allWidgets =
+            mapOf(
+                // app 1
+                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),
+                    ),
+                // 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)),
+            )
+    }
+
+    @Test
+    fun widgetsListBaseEntriesBuilder_addsHeaderAndContentEntries_withCorrectSectionName() {
+        val expectedWidgetsCountBySection =
+            listOf(
+                APP_1_EXPECTED_SECTION_NAME to 2,
+                APP_2_EXPECTED_SECTION_NAME to 1,
+                APP_3_EXPECTED_SECTION_NAME to 1,
+            )
+
+        val entries = underTest.build(allWidgets)
+
+        assertThat(entries).hasSize(6)
+        val headerEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListHeaderEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        val contentEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListContentEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        assertThat(headerEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+        assertThat(contentEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+    }
+
+    @Test
+    fun widgetsListBaseEntriesBuilder_withFilter_addsFilteredHeaderAndContentEntries() {
+        val allowList = listOf(APP_1_PROVIDER_1_CLASS_NAME, APP_3_PROVIDER_1_CLASS_NAME)
+        val expectedWidgetsCountBySection =
+            listOf(
+                APP_1_EXPECTED_SECTION_NAME to 1, // one widget filtered out
+                APP_3_EXPECTED_SECTION_NAME to 1,
+            )
+
+        val entries =
+            underTest.build(allWidgets) { w -> allowList.contains(w.componentName.shortClassName) }
+
+        assertThat(entries).hasSize(4) // app 2 filtered out
+        val headerEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListHeaderEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        val contentEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListContentEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        assertThat(headerEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+        assertThat(contentEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+    }
+
+    private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
+        val packageItemInfo = PackageItemInfo(packageName, userHandle)
+        packageItemInfo.title = title
+        return packageItemInfo
+    }
+
+    private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
+        val providerInfo =
+            WidgetUtils.createAppWidgetProviderInfo(
+                ComponentName.createRelative(packageName, widgetProviderName)
+            )
+        val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+        return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+    }
+
+    companion object {
+        const val APP_1_PACKAGE_NAME = "com.example.app1"
+        const val APP_1_PACKAGE_TITLE = "App1"
+        const val APP_1_EXPECTED_SECTION_NAME = "A" // for fast popup
+        const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
+        const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
+
+        const val APP_2_PACKAGE_NAME = "com.example.app2"
+        const val APP_2_PACKAGE_TITLE = "SomeApp2"
+        const val APP_2_EXPECTED_SECTION_NAME = "S" // for fast popup
+        const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
+
+        const val APP_3_PACKAGE_NAME = "com.example.app3"
+        const val APP_3_PACKAGE_TITLE = "OtherApp3"
+        const val APP_3_EXPECTED_SECTION_NAME = "O" // for fast popup
+        const val APP_3_PROVIDER_1_CLASS_NAME = "app3Provider1"
+    }
+}
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
new file mode 100644
index 0000000..1da74cb
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+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
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener
+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.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+// Tests for the WidgetPickerDataProvider class
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetPickerDataProviderTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var changeListener: WidgetPickerDataChangeListener
+
+    @Mock private lateinit var iconCache: IconCache
+
+    private lateinit var userHandle: UserHandle
+    private lateinit var context: Context
+    private lateinit var testInvariantProfile: InvariantDeviceProfile
+
+    private lateinit var appWidgetItem: WidgetItem
+
+    private var underTest = WidgetPickerDataProvider()
+
+    @Before
+    fun setUp() {
+        userHandle = UserHandle.CURRENT
+        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        testInvariantProfile = LauncherAppState.getIDP(context)
+
+        doAnswer { invocation: InvocationOnMock ->
+                val componentWithLabel = invocation.getArgument<Any>(0) as CachedObject
+                componentWithLabel.getComponent().shortClassName
+            }
+            .`when`(iconCache)
+            .getTitleNoCache(any<CachedObject>())
+
+        appWidgetItem = createWidgetItem()
+    }
+
+    @Test
+    fun setWidgets_invokesTheListener_andUpdatedWidgetsAvailable() {
+        assertThat(underTest.get().allWidgets).isEmpty()
+
+        underTest.setChangeListener(changeListener)
+        val allWidgets = appWidgetListBaseEntries()
+        underTest.setWidgets(allWidgets = allWidgets)
+
+        assertThat(underTest.get().allWidgets).containsExactlyElementsIn(allWidgets)
+        verify(changeListener, times(1)).onWidgetsBound()
+        verifyNoMoreInteractions(changeListener)
+    }
+
+    @Test
+    fun setWidgetRecommendations_callsBackTheListener_andUpdatedRecommendationsAvailable() {
+        underTest.setWidgets(allWidgets = appWidgetListBaseEntries())
+        assertThat(underTest.get().recommendations).isEmpty()
+
+        underTest.setChangeListener(changeListener)
+        val recommendations =
+            listOf(
+                PendingAddWidgetInfo(
+                    appWidgetItem.widgetInfo,
+                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION,
+                )
+            )
+        underTest.setWidgetRecommendations(recommendations)
+
+        assertThat(underTest.get().recommendations).hasSize(1)
+        verify(changeListener, times(1)).onRecommendedWidgetsBound()
+        verifyNoMoreInteractions(changeListener)
+    }
+
+    @Test
+    fun setChangeListener_null_noCallback() {
+        underTest.setChangeListener(changeListener)
+        underTest.setChangeListener(null) // reset
+
+        underTest.setWidgets(allWidgets = appWidgetListBaseEntries())
+        val recommendations =
+            listOf(
+                PendingAddWidgetInfo(
+                    appWidgetItem.widgetInfo,
+                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION,
+                )
+            )
+        underTest.setWidgetRecommendations(recommendations)
+
+        verifyNoMoreInteractions(changeListener)
+    }
+
+    private fun createWidgetItem(): WidgetItem {
+        val providerInfo =
+            WidgetUtils.createAppWidgetProviderInfo(
+                ComponentName.createRelative(APP_PACKAGE_NAME, APP_PROVIDER_1_CLASS_NAME)
+            )
+        val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+        return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+    }
+
+    private fun appWidgetListBaseEntries(): List<WidgetsListBaseEntry> {
+        val packageItemInfo = PackageItemInfo(APP_PACKAGE_NAME, userHandle)
+        packageItemInfo.title = APP_PACKAGE_TITLE
+        val widgets = listOf(appWidgetItem)
+
+        return buildList {
+            add(WidgetsListHeaderEntry.create(packageItemInfo, APP_SECTION_NAME, widgets))
+            add(WidgetsListContentEntry(packageItemInfo, APP_SECTION_NAME, widgets))
+        }
+    }
+
+    companion object {
+        const val APP_PACKAGE_NAME = "com.example.app"
+        const val APP_PACKAGE_TITLE = "SomeApp"
+        const val APP_SECTION_NAME = "S" // for fast popup
+        const val APP_PROVIDER_1_CLASS_NAME = "appProvider1"
+    }
+}
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
new file mode 100644
index 0000000..deec67a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
@@ -0,0 +1,377 @@
+/*
+ * 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.model.data
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+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
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets
+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.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+
+// Tests for code / classes in WidgetPickerData file.
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetPickerDataTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var iconCache: IconCache
+
+    private lateinit var userHandle: UserHandle
+    private lateinit var context: Context
+    private lateinit var testInvariantProfile: InvariantDeviceProfile
+
+    private lateinit var app1PackageItemInfo: PackageItemInfo
+    private lateinit var app2PackageItemInfo: PackageItemInfo
+
+    private lateinit var app1WidgetItem1: WidgetItem
+    private lateinit var app1WidgetItem2: WidgetItem
+    private lateinit var app2WidgetItem1: WidgetItem
+
+    @Before
+    fun setUp() {
+        userHandle = UserHandle.CURRENT
+        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        testInvariantProfile = LauncherAppState.getIDP(context)
+
+        doAnswer { invocation: InvocationOnMock ->
+                val componentWithLabel = invocation.getArgument<Any>(0) as CachedObject
+                componentWithLabel.getComponent().shortClassName
+            }
+            .`when`(iconCache)
+            .getTitleNoCache(any<CachedObject>())
+
+        app1PackageItemInfo = packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE)
+        app2PackageItemInfo = packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE)
+
+        app1WidgetItem1 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME)
+        app1WidgetItem2 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
+        app2WidgetItem1 = createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)
+    }
+
+    @Test
+    fun withWidgets_returnsACopyWithProvidedWidgets() {
+        // only app two
+        val widgetPickerData = WidgetPickerData(allWidgets = appTwoWidgetsListBaseEntries())
+
+        // update: only app 1 and default list set
+        val newAllWidgets: List<WidgetsListBaseEntry> =
+            appOneWidgetsListBaseEntries(includeWidgetTwo = true)
+        val newDefaultWidgets: List<WidgetsListBaseEntry> =
+            appOneWidgetsListBaseEntries(includeWidgetTwo = false)
+
+        val newWidgetData = widgetPickerData.withWidgets(newAllWidgets, newDefaultWidgets)
+
+        assertThat(newWidgetData.allWidgets).containsExactlyElementsIn(newAllWidgets)
+        assertThat(newWidgetData.defaultWidgets).containsExactlyElementsIn(newDefaultWidgets)
+    }
+
+    @Test
+    fun withWidgets_noExplicitDefaults_unsetsOld() {
+        // only app two
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets = appTwoWidgetsListBaseEntries(),
+                defaultWidgets = appTwoWidgetsListBaseEntries(),
+            )
+
+        val newWidgetData =
+            widgetPickerData.withWidgets(allWidgets = appOneWidgetsListBaseEntries())
+
+        assertThat(newWidgetData.allWidgets)
+            .containsExactlyElementsIn(appOneWidgetsListBaseEntries())
+        assertThat(newWidgetData.defaultWidgets).isEmpty() // previous values cleared.
+    }
+
+    @Test
+    fun withRecommendedWidgets_returnsACopyWithProvidedRecommendedWidgets() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+            )
+        val recommendations: List<ItemInfo> =
+            listOf(
+                PendingAddWidgetInfo(
+                    app1WidgetItem1.widgetInfo,
+                    CONTAINER_WIDGETS_PREDICTION,
+                    CATEGORY_1,
+                ),
+                PendingAddWidgetInfo(
+                    app2WidgetItem1.widgetInfo,
+                    CONTAINER_WIDGETS_PREDICTION,
+                    CATEGORY_2,
+                ),
+            )
+
+        val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+        assertThat(updatedData.recommendations.keys).containsExactly(CATEGORY_1, CATEGORY_2)
+        assertThat(updatedData.recommendations[CATEGORY_1]).containsExactly(app1WidgetItem1)
+        assertThat(updatedData.recommendations[CATEGORY_2]).containsExactly(app2WidgetItem1)
+    }
+
+    @Test
+    fun withRecommendedWidgets_noCategory_usesDefault() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+            )
+        val recommendations: List<ItemInfo> =
+            listOf(
+                PendingAddWidgetInfo(app1WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+                PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+            )
+
+        val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+        assertThat(updatedData.recommendations.keys)
+            .containsExactly(DEFAULT_WIDGET_RECOMMENDATION_CATEGORY)
+        assertThat(updatedData.recommendations[DEFAULT_WIDGET_RECOMMENDATION_CATEGORY])
+            .containsExactly(app1WidgetItem1, app2WidgetItem1)
+    }
+
+    @Test
+    fun withRecommendedWidgets_emptyRecommendations_clearsOld() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+                recommendations = mapOf(CATEGORY_1 to listOf(app1WidgetItem1)),
+            )
+
+        val updatedData = widgetPickerData.withRecommendedWidgets(listOf())
+
+        assertThat(updatedData.recommendations).isEmpty()
+    }
+
+    @Test
+    fun withRecommendedWidgets_widgetNotInAllWidgets_filteredOut() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false))
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+            )
+
+        val recommendations: List<ItemInfo> =
+            listOf(
+                PendingAddWidgetInfo(app1WidgetItem2.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+                PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+            )
+        val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+        assertThat(updatedData.recommendations).hasSize(1)
+        // no app1widget2
+        assertThat(updatedData.recommendations.values.first()).containsExactly(app2WidgetItem1)
+    }
+
+    @Test
+    fun findContentEntryForPackageUser_returnsCorrectEntry() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) },
+            )
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+        val contentEntry = findContentEntryForPackageUser(widgetPickerData, app1PackageUserKey)
+
+        assertThat(contentEntry).isNotNull()
+        assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
+        assertThat(contentEntry?.mWidgets).hasSize(2)
+    }
+
+    @Test
+    fun findContentEntryForPackageUser_fromDefaults_returnsEntryFromDefaultWidgets() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets =
+                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) },
+            )
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+        val contentEntry =
+            findContentEntryForPackageUser(
+                widgetPickerData = widgetPickerData,
+                packageUserKey = app1PackageUserKey,
+                fromDefaultWidgets = true,
+            )
+
+        assertThat(contentEntry).isNotNull()
+        assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
+        // only one widget (since default widgets had only one widget for app A
+        assertThat(contentEntry?.mWidgets).hasSize(1)
+    }
+
+    @Test
+    fun findContentEntryForPackageUser_noMatch_returnsNull() {
+        val app2PackageUserKey = PackageUserKey.fromPackageItemInfo(app2PackageItemInfo)
+        val widgetPickerData =
+            WidgetPickerData(allWidgets = buildList { addAll(appOneWidgetsListBaseEntries()) })
+
+        val contentEntry = findContentEntryForPackageUser(widgetPickerData, app2PackageUserKey)
+
+        assertThat(contentEntry).isNull()
+    }
+
+    @Test
+    fun findAllWidgetsForPackageUser_returnsListOfWidgets() {
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets =
+                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) },
+            )
+
+        val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
+
+        // both widgets returned irrespective of default widgets list
+        assertThat(widgets).hasSize(2)
+    }
+
+    @Test
+    fun findAllWidgetsForPackageUser_noMatch_returnsEmptyList() {
+        val widgetPickerData =
+            WidgetPickerData(allWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) })
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+        val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
+
+        assertThat(widgets).isEmpty()
+    }
+
+    private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
+        val packageItemInfo = PackageItemInfo(packageName, userHandle)
+        packageItemInfo.title = title
+        return packageItemInfo
+    }
+
+    private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
+        val providerInfo =
+            WidgetUtils.createAppWidgetProviderInfo(
+                ComponentName.createRelative(packageName, widgetProviderName)
+            )
+        val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+        return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+    }
+
+    private fun appTwoWidgetsListBaseEntries(): List<WidgetsListBaseEntry> = buildList {
+        val widgets = listOf(app2WidgetItem1)
+        add(WidgetsListHeaderEntry.create(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
+        add(WidgetsListContentEntry(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
+    }
+
+    private fun appOneWidgetsListBaseEntries(
+        includeWidgetTwo: Boolean = true
+    ): List<WidgetsListBaseEntry> = buildList {
+        val widgets =
+            if (includeWidgetTwo) {
+                listOf(app1WidgetItem1, app1WidgetItem2)
+            } else {
+                listOf(app1WidgetItem1)
+            }
+
+        add(WidgetsListHeaderEntry.create(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
+        add(WidgetsListContentEntry(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
+    }
+
+    companion object {
+        private const val APP_1_PACKAGE_NAME = "com.example.app1"
+        private const val APP_1_PACKAGE_TITLE = "App1"
+        private const val APP_1_SECTION_NAME = "A" // for fast popup
+        private const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
+        private const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
+
+        private const val APP_2_PACKAGE_NAME = "com.example.app2"
+        private const val APP_2_PACKAGE_TITLE = "SomeApp2"
+        private const val APP_2_SECTION_NAME = "S" // for fast popup
+        private const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
+
+        private val CATEGORY_1 =
+            WidgetRecommendationCategory(/* categoryTitleRes= */ 0, /* order= */ 0)
+        private val CATEGORY_2 =
+            WidgetRecommendationCategory(/* categoryTitleRes= */ 1, /* order= */ 1)
+    }
+}
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 0370a6b..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,16 +41,16 @@
 
 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.popup.PopupDataProvider;
 import com.android.launcher3.search.SearchCallback;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -79,7 +79,7 @@
 
     private SimpleWidgetsSearchAlgorithm mSimpleWidgetsSearchAlgorithm;
     @Mock
-    private PopupDataProvider mDataProvider;
+    private WidgetsSearchDataProvider mDataProvider;
     @Mock
     private SearchCallback<WidgetsListBaseEntry> mSearchCallback;
 
@@ -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();
@@ -106,7 +106,7 @@
 
         mSimpleWidgetsSearchAlgorithm = MAIN_EXECUTOR.submit(
                 () -> new SimpleWidgetsSearchAlgorithm(mDataProvider)).get();
-        doReturn(Collections.EMPTY_LIST).when(mDataProvider).getAllWidgets();
+        doReturn(Collections.EMPTY_LIST).when(mDataProvider).getWidgets();
     }
 
     @Test
@@ -114,7 +114,7 @@
         doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                 mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
                 .when(mDataProvider)
-                .getAllWidgets();
+                .getWidgets();
 
         assertEquals(List.of(
                 WidgetsListHeaderEntry.createForSearch(
@@ -135,7 +135,7 @@
         doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                 mCameraContentEntry))
                 .when(mDataProvider)
-                .getAllWidgets();
+                .getWidgets();
 
         assertEquals(List.of(
                 WidgetsListHeaderEntry.createForSearch(
@@ -162,7 +162,7 @@
         doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                 mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
                 .when(mDataProvider)
-                .getAllWidgets();
+                .getWidgets();
         mSimpleWidgetsSearchAlgorithm.doSearch("Ca", mSearchCallback);
         getInstrumentation().waitForIdleSync();
         verify(mSearchCallback).onSearchResult(
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt
index 7b629bf..7a858e4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt
@@ -85,7 +85,8 @@
                 Point(1, 1) to WidgetPreviewContainerSize(1, 1),
                 // 2x1
                 Point(2, 1) to WidgetPreviewContainerSize(2, 1),
-                Point(3, 1) to WidgetPreviewContainerSize(2, 1),
+                // 3x1
+                Point(3, 1) to WidgetPreviewContainerSize(3, 1),
                 // 4x1
                 Point(4, 1) to WidgetPreviewContainerSize(4, 1),
                 // 2x2
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/quickstep/res/drawable/bg_circle.xml b/tests/res/drawable/test_app_info_icon.xml
similarity index 60%
copy from quickstep/res/drawable/bg_circle.xml
copy to tests/res/drawable/test_app_info_icon.xml
index 506177b..2e824ac 100644
--- a/quickstep/res/drawable/bg_circle.xml
+++ b/tests/res/drawable/test_app_info_icon.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2020 The Android Open Source Project
+Copyright (C) 2024 The Android Open Source Project
 
-    Licensed under the Apache License, Version 2.0 (the "License");
+   Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at
 
@@ -14,7 +14,9 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="oval">
-    <solid android:color="#FFFFFFFF" />
-</shape>
\ No newline at end of file
+<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/quickstep/res/drawable/bg_circle.xml b/tests/res/drawable/test_different_activity_icon.xml
similarity index 60%
copy from quickstep/res/drawable/bg_circle.xml
copy to tests/res/drawable/test_different_activity_icon.xml
index 506177b..43d3611 100644
--- a/quickstep/res/drawable/bg_circle.xml
+++ b/tests/res/drawable/test_different_activity_icon.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2020 The Android Open Source Project
+Copyright (C) 2024 The Android Open Source Project
 
-    Licensed under the Apache License, Version 2.0 (the "License");
+   Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at
 
@@ -14,7 +14,9 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="oval">
-    <solid android:color="#FFFFFFFF" />
-</shape>
\ No newline at end of file
+<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/FloatingMaskViewTest.kt b/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt
new file mode 100644
index 0000000..cf03adc
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.allapps
+
+import android.content.Context
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.ImageView
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+class FloatingMaskViewTest {
+    @Mock
+    private val mockAllAppsRecyclerView: AllAppsRecyclerView? = null
+
+    @Mock
+    private val mockBottomBox: ImageView? = null
+    private var mVut: FloatingMaskView? = null
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val context: Context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        mVut = FloatingMaskView(context)
+        mVut!!.layoutParams = MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT)
+    }
+
+    @Test
+    fun setParameters_paramsMarginEqualRecyclerViewPadding() {
+        val floatingMaskView = Mockito.spy(mVut)
+        Mockito.`when`(mockAllAppsRecyclerView!!.paddingLeft).thenReturn(PADDING_PX)
+        Mockito.`when`(mockAllAppsRecyclerView.paddingRight).thenReturn(PADDING_PX)
+        Mockito.`when`(mockAllAppsRecyclerView.paddingBottom).thenReturn(PADDING_PX)
+        Mockito.`when`(floatingMaskView!!.bottomBox).thenReturn(mockBottomBox)
+        val lp = floatingMaskView.layoutParams as MarginLayoutParams
+
+        floatingMaskView.setParameters(lp, mockAllAppsRecyclerView)
+
+        Truth.assertThat(lp.leftMargin).isEqualTo(PADDING_PX)
+        Truth.assertThat(lp.rightMargin).isEqualTo(PADDING_PX)
+        Mockito.verify(mockBottomBox)?.minimumHeight = PADDING_PX
+    }
+
+    companion object {
+        private const val PADDING_PX = 15
+    }
+}
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/PrivateSpaceHeaderViewTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
index eac7f63..398f9c5 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
@@ -86,6 +86,8 @@
     private static final String CAMERA_PACKAGE_NAME = "com.android.launcher3.tests.camera";
     private static final int CONTAINER_HEADER_ELEMENT_COUNT = 1;
     private static final int LOCK_UNLOCK_BUTTON_COUNT = 1;
+    private static final int MAIN_USER_APP_COUNT = 1;
+    private static final int VIEW_AT_END_OF_APP_LIST = 1;
     private static final int PS_SETTINGS_BUTTON_COUNT_VISIBLE = 1;
     private static final int PS_SETTINGS_BUTTON_COUNT_INVISIBLE = 0;
     private static final int PS_TRANSITION_IMAGE_COUNT = 1;
@@ -116,7 +118,8 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = new ActivityContextWrapper(getApplicationContext());
+        mContext = new ActivityContextWrapper(getApplicationContext(),
+                R.style.DynamicColorsBaseLauncherTheme);
         when(mAllApps.getContext()).thenReturn(mContext);
         when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO);
         when(mUserCache.getUserProfiles())
@@ -299,8 +302,8 @@
         int rows = (int) (ALL_APPS_HEIGHT - PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT);
         int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
 
-        // The number of adapterItems should be the private space apps + one main app + header.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT
+                        + CONTAINER_HEADER_ELEMENT_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(position,
                 privateProfileManager.scrollForHeaderToBeVisibleInContainer(
@@ -334,8 +337,8 @@
         int rows = (int) (ALL_APPS_HEIGHT - PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT) - 1;
         int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
 
-        // The number of adapterItems should be the private space apps + one main app + header.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT
+                        + CONTAINER_HEADER_ELEMENT_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(position,
                 privateProfileManager.scrollForHeaderToBeVisibleInContainer(
@@ -369,8 +372,8 @@
         int rows = (int) (ALL_APPS_HEIGHT - BIGGER_PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT);
         int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
 
-        // The number of adapterItems should be the private space apps + one main app + header.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT
+                        + CONTAINER_HEADER_ELEMENT_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(position,
                 privateProfileManager.scrollForHeaderToBeVisibleInContainer(
@@ -398,8 +401,7 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        // The number of adapterItems should be the private space apps + one main app.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(SCROLL_NO_WHERE, privateProfileManager.scrollForHeaderToBeVisibleInContainer(
                 new AllAppsRecyclerView(mContext),
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/allapps/TaplOpenCloseAllAppsTest.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
index 05a1224..c7c9dbb 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
@@ -25,7 +25,6 @@
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.launcher3.Launcher;
@@ -33,7 +32,6 @@
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
-import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.Test;
 
@@ -191,7 +189,6 @@
     /**
      * Makes sure that when pressing back when AllApps is open we go back to the Home screen.
      */
-    @FlakyTest(bugId = 256615483)
     @Test
     @PortraitLandscape
     public void testPressBackFromAllAppsToHome() {
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/celllayout/integrationtest/TestUtils.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
index 4cecb5a..bcb9191 100644
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
@@ -21,6 +21,7 @@
 import android.view.View
 import android.view.ViewGroup
 import com.android.launcher3.CellLayout
+import com.android.launcher3.Utilities
 import com.android.launcher3.Workspace
 import com.android.launcher3.util.CellAndSpan
 import com.android.launcher3.widget.LauncherAppWidgetHostView
@@ -54,7 +55,7 @@
         return view as LauncherAppWidgetHostView
     }
 
-    fun getCellTopLeftRelativeToCellLayout(
+    fun getCellTopLeftRelativeToWorkspace(
         workspace: Workspace<*>,
         cellAndSpan: CellAndSpan
     ): Point {
@@ -67,6 +68,8 @@
             cellAndSpan.spanY,
             target
         )
-        return Point(target.left, target.top)
+        val point = floatArrayOf(target.left.toFloat(), target.top.toFloat())
+        Utilities.getDescendantCoordRelativeToAncestor(cellLayout, workspace, point, false)
+        return Point(point[0].toInt(), point[1].toInt())
     }
 }
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
index 365ad4b..5e062d0 100644
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
@@ -35,9 +35,10 @@
 
     companion object {
         private const val TAG = "EventWaiter"
+        private val SIGNAL_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
     }
 
-    fun waitForSignal(timeout: Long = TimeUnit.SECONDS.toMillis(10)) = runBlocking {
+    fun waitForSignal(timeout: Long = SIGNAL_TIMEOUT) = runBlocking {
         var status = withTimeoutOrNull(timeout) { deferrable.await() }
         if (status == null) {
             status = EventStatus.TIMEOUT
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 41abcf8..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;
@@ -174,13 +173,13 @@
     public void testDragAndCancelAppIcon() {
         final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(GMAIL_APP_NAME);
         Point positionBeforeDrag =
-                mLauncher.getWorkspace().getWorkspaceIconsPositions().get(GMAIL_APP_NAME);
+                mLauncher.getWorkspace().getWorkspaceIconPosition(GMAIL_APP_NAME);
         assertNotNull("App not found in Workspace before dragging.", positionBeforeDrag);
 
         mLauncher.getWorkspace().dragAndCancelAppIcon(homeAppIcon);
 
         Point positionAfterDrag =
-                mLauncher.getWorkspace().getWorkspaceIconsPositions().get(GMAIL_APP_NAME);
+                mLauncher.getWorkspace().getWorkspaceIconPosition(GMAIL_APP_NAME);
         assertNotNull("App not found in Workspace after dragging.", positionAfterDrag);
         assertEquals("App not returned to same position in Workspace after drag & cancel",
                 positionBeforeDrag, positionAfterDrag);
@@ -195,7 +194,6 @@
     @PlatinumTest(focusArea = "launcher")
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/343953783
     public void testDragAppIcon() {
 
         final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
@@ -228,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 46cafa7..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,13 +37,11 @@
 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;
 
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.Map;
 
 /**
  * Test runs in Out of process (Oop) and In process (Ipc)
@@ -60,6 +55,7 @@
      */
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/349439239
     public void testDeleteFromWorkspace() {
         for (String appName : new String[]{GMAIL_APP_NAME, STORE_APP_NAME, TEST_APP_NAME}) {
             final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(appName);
@@ -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 {
@@ -98,7 +93,6 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/311099513
     public void testUninstallFromWorkspace() throws Exception {
         installDummyAppAndWaitForUIUpdate();
         try {
@@ -151,22 +145,17 @@
                     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]);
             }
 
-            Map<String, Point> initialPositions =
-                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
-            assertThat(initialPositions.keySet()).containsAtLeastElementsIn(appNames);
+            Point initialPosition =
+                    mLauncher.getWorkspace().getWorkspaceIconPosition(DUMMY_APP_NAME);
+            assertThat(initialPosition).isNotNull();
 
             final Workspace workspace = mLauncher.getWorkspace().getWorkspaceAppIcon(
                     DUMMY_APP_NAME).uninstall();
             workspace.verifyWorkspaceAppIconIsGone(
                     DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
-
-            Log.d(UIOBJECT_STALE_ELEMENT, "second getWorkspaceIconsPositions()");
-            Map<String, Point> finalPositions = workspace.getWorkspaceIconsPositions();
-            assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
         } finally {
             TestUtil.uninstallDummyApp();
         }
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
deleted file mode 100644
index e1daa74..0000000
--- a/tests/src/com/android/launcher3/folder/FolderTest.kt
+++ /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.folder
-
-import android.content.Context
-import android.graphics.Point
-import android.view.View
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.launcher3.DropTarget.DragObject
-import com.android.launcher3.LauncherAppState
-import com.android.launcher3.celllayout.board.FolderPoint
-import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
-import com.android.launcher3.util.ActivityContextWrapper
-import com.android.launcher3.util.ModelTestExtensions.clearModelDb
-import junit.framework.TestCase.assertEquals
-import org.junit.After
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-
-/** Tests for [Folder] */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class FolderTest {
-
-    private val context: Context =
-        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
-    private val workspaceBuilder = TestWorkspaceBuilder(context)
-    private val folder: Folder = Mockito.spy(Folder(context, null))
-
-    @After
-    fun tearDown() {
-        LauncherAppState.getInstance(context).model.clearModelDb()
-    }
-
-    @Test
-    fun `Undo a folder with 1 icon when onDropCompleted is called`() {
-        val folderInfo =
-            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
-        folder.mInfo = folderInfo
-        folder.mInfo.getContents().removeAt(0)
-        folder.mContent = Mockito.mock(FolderPagedView::class.java)
-        val dragLayout = Mockito.mock(View::class.java)
-        val dragObject = Mockito.mock(DragObject::class.java)
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
-        folder.onDropCompleted(dragLayout, dragObject, true)
-        verify(folder, times(1)).replaceFolderWithFinalItem()
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
-    }
-
-    @Test
-    fun `Do not undo a folder with 2 icons when onDropCompleted is called`() {
-        val folderInfo =
-            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
-        folder.mInfo = folderInfo
-        folder.mContent = Mockito.mock(FolderPagedView::class.java)
-        val dragLayout = Mockito.mock(View::class.java)
-        val dragObject = Mockito.mock(DragObject::class.java)
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
-        folder.onDropCompleted(dragLayout, dragObject, true)
-        verify(folder, times(0)).replaceFolderWithFinalItem()
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
-    }
-
-    companion object {
-        const val TWO_ICON_FOLDER_TYPE = 'A'
-    }
-}
diff --git a/tests/src/com/android/launcher3/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
new file mode 100644
index 0000000..d9af07a
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
@@ -0,0 +1,235 @@
+/*
+ * 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.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherActivityInfo
+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 com.android.launcher3.AppFilter
+import com.android.launcher3.Flags.FLAG_ENABLE_PRIVATE_SPACE
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.PackageUpdatedTask.OP_ADD
+import com.android.launcher3.model.PackageUpdatedTask.OP_REMOVE
+import com.android.launcher3.model.PackageUpdatedTask.OP_SUSPEND
+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.model.PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.TestUtil
+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.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class PackageUpdatedTaskTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val mUser = UserHandle(0)
+    private val mDataModel: BgDataModel = BgDataModel()
+    private val mLauncherModelHelper = LauncherModelHelper()
+    private val mContext: SandboxModelContext = spy(mLauncherModelHelper.sandboxContext)
+    private val mAppState: LauncherAppState = spy(LauncherAppState.getInstance(mContext))
+
+    private val expectedPackage = "Test.Package"
+    private val expectedComponent = ComponentName(expectedPackage, "TestClass")
+    private val expectedActivityInfo: LauncherActivityInfo = mock<LauncherActivityInfo>()
+    private val expectedWorkspaceItem = spy(WorkspaceItemInfo())
+
+    private val mockIconCache: IconCache = mock()
+    private val mockTaskController: ModelTaskController = mock<ModelTaskController>()
+    private val mockAppFilter: AppFilter = mock<AppFilter>()
+    private val mockApplicationInfo: ApplicationInfo = mock<ApplicationInfo>()
+    private val mockActivityInfo: ActivityInfo = mock<ActivityInfo>()
+
+    private lateinit var mAllAppsList: AllAppsList
+
+    @Before
+    fun setup() {
+        mAllAppsList = spy(AllAppsList(mockIconCache, mockAppFilter))
+        mLauncherModelHelper.sandboxContext.spyService(LauncherApps::class.java).apply {
+            whenever(getActivityList(expectedPackage, mUser))
+                .thenReturn(listOf(expectedActivityInfo))
+        }
+        whenever(mAppState.iconCache).thenReturn(mockIconCache)
+        whenever(mockTaskController.app).thenReturn(mAppState)
+        whenever(mockAppFilter.shouldShowApp(expectedComponent)).thenReturn(true)
+        mockApplicationInfo.apply {
+            uid = 1
+            isArchived = false
+        }
+        mockActivityInfo.isArchived = false
+        expectedActivityInfo.apply {
+            whenever(applicationInfo).thenReturn(mockApplicationInfo)
+            whenever(activityInfo).thenReturn(mockActivityInfo)
+            whenever(componentName).thenReturn(expectedComponent)
+        }
+        expectedWorkspaceItem.apply {
+            itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+            user = mUser
+            whenever(targetPackage).thenReturn(expectedPackage)
+            whenever(targetComponent).thenReturn(expectedComponent)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mLauncherModelHelper.destroy()
+    }
+
+    @Test
+    fun `OP_ADD triggers model callbacks and adds new items to AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_ADD, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mockIconCache).updateIconsForPkg(expectedPackage, mUser)
+        verify(mAllAppsList).addPackage(mContext, expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        verify(mockTaskController).bindUpdatedWidgets(mDataModel)
+        assertThat(mAllAppsList.data.firstOrNull()?.componentName)
+            .isEqualTo(AppInfo(mContext, expectedActivityInfo, mUser).componentName)
+    }
+
+    @Test
+    fun `OP_UPDATE triggers model callbacks and updates items in AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_UPDATE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mockIconCache).updateIconsForPkg(expectedPackage, mUser)
+        verify(mAllAppsList).updatePackage(mContext, expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.data.firstOrNull()?.componentName)
+            .isEqualTo(AppInfo(mContext, expectedActivityInfo, mUser).componentName)
+    }
+
+    @Test
+    fun `OP_REMOVE triggers model callbacks and removes packages and icons`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_REMOVE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mockIconCache).removeIconsForPkg(expectedPackage, mUser)
+        verify(mAllAppsList).removePackage(expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.data).isEmpty()
+    }
+
+    @Test
+    fun `OP_UNAVAILABLE triggers model callbacks and removes package from AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_UNAVAILABLE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).removePackage(expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.data).isEmpty()
+    }
+
+    @Test
+    fun `OP_SUSPEND triggers model callbacks and updates flags in AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_SUSPEND, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        mAllAppsList.add(AppInfo(mContext, expectedActivityInfo, mUser), expectedActivityInfo)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).updateDisabledFlags(any(), any())
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.getAndResetChangeFlag()).isTrue()
+    }
+
+    @Test
+    fun `OP_UNSUSPEND triggers no callbacks when app not suspended`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_UNSUSPEND, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).updateDisabledFlags(any(), any())
+        verify(mockTaskController).bindUpdatedWorkspaceItems(emptyList())
+        assertThat(mAllAppsList.getAndResetChangeFlag()).isFalse()
+    }
+
+    @EnableFlags(FLAG_ENABLE_PRIVATE_SPACE)
+    @Test
+    fun `OP_USER_AVAILABILITY_CHANGE triggers no callbacks if current user not work or private`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_USER_AVAILABILITY_CHANGE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).updateDisabledFlags(any(), any())
+        verify(mockTaskController).bindUpdatedWorkspaceItems(emptyList())
+        assertThat(mAllAppsList.data).isEmpty()
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
new file mode 100644
index 0000000..d553f47
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
@@ -0,0 +1,315 @@
+/*
+ * 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.appwidget.AppWidgetProviderInfo
+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
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.util.LongSparseArray
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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.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
+import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_UI_NOT_READY
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.UserIconInfo
+import com.android.launcher3.widget.WidgetInflater
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class WorkspaceItemProcessorExtraTest {
+
+    @Mock private lateinit var mockIconRequestInfo: IconRequestInfo<WorkspaceItemInfo>
+    @Mock private lateinit var mockWorkspaceInfo: WorkspaceItemInfo
+    @Mock private lateinit var mockBgDataModel: BgDataModel
+    @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var mockAppState: LauncherAppState
+    @Mock private lateinit var mockPmHelper: PackageManagerHelper
+    @Mock private lateinit var mockLauncherApps: LauncherApps
+    @Mock private lateinit var mockCursor: LoaderCursor
+    @Mock private lateinit var mockUserCache: UserCache
+    @Mock private lateinit var mockUserManagerState: UserManagerState
+    @Mock private lateinit var mockWidgetInflater: WidgetInflater
+
+    private var intent: Intent = Intent()
+    private var mUserHandle: UserHandle = UserHandle(0)
+    private var mIconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mutableListOf()
+    private var mComponentName: ComponentName = ComponentName("package", "class")
+    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<CacheableShortcutInfo> = mutableListOf()
+    private var mWidgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> =
+        mutableMapOf()
+    private var mPendingPackages: MutableSet<PackageUserKey> = mutableSetOf()
+
+    private lateinit var itemProcessorUnderTest: WorkspaceItemProcessor
+
+    @Before
+    fun setup() {
+        mUserHandle = UserHandle(0)
+        mockIconRequestInfo = mock<IconRequestInfo<WorkspaceItemInfo>>()
+        mockWorkspaceInfo = mock<WorkspaceItemInfo>()
+        mockBgDataModel = mock<BgDataModel>()
+        mComponentName = ComponentName("package", "class")
+        mUnlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
+        intent =
+            Intent().apply {
+                component = mComponentName
+                `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 {
+                whenever(context).thenReturn(mockContext)
+                whenever(iconCache).thenReturn(mock())
+                whenever(iconCache.getShortcutIcon(any(), any(), any())).then {}
+            }
+        mockPmHelper =
+            mock<PackageManagerHelper>().apply {
+                whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
+                    .thenReturn(intent)
+            }
+        mockCursor =
+            Mockito.mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
+                user = mUserHandle
+                itemType = ITEM_TYPE_APPLICATION
+                id = 1
+                restoreFlag = 1
+                serialNumber = 101
+                whenever(parseIntent()).thenReturn(intent)
+                whenever(markRestored()).doAnswer { restoreFlag = 0 }
+                whenever(updater().put(Favorites.INTENT, intent.toUri(0)).commit()).thenReturn(1)
+                whenever(getAppShortcutInfo(any(), any(), any(), any()))
+                    .thenReturn(mockWorkspaceInfo)
+                whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
+            }
+        mockUserCache =
+            mock<UserCache>().apply {
+                val userIconInfo =
+                    mock<UserIconInfo>().apply { whenever(isPrivate).thenReturn(false) }
+                whenever(getUserInfo(any())).thenReturn(userIconInfo)
+            }
+
+        mockUserManagerState = mock<UserManagerState>()
+        mockWidgetInflater = mock<WidgetInflater>()
+        mKeyToPinnedShortcutsMap = mutableMapOf()
+        mInstallingPkgs = hashMapOf()
+        mAllDeepShortcuts = mutableListOf()
+        mWidgetProvidersMap = mutableMapOf()
+        mIconRequestInfos = mutableListOf()
+        mPendingPackages = mutableSetOf()
+    }
+
+    @Test
+    fun `When Pending App Widget has not started restore then update db and add item`() {
+        // 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)
+                    .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`() {
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
+        val expectedPackage = expectedComponentName!!.packageName
+        val expectedUser = UserHandle(1)
+
+        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)
+
+        // When
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        verify(mockCursor).checkAndAddItem(any(), any())
+    }
+
+    private fun createWorkspaceItemProcessorUnderTest(
+        cursor: LoaderCursor = mockCursor,
+        memoryLogger: LoaderMemoryLogger? = null,
+        userCache: UserCache = mockUserCache,
+        userManagerState: UserManagerState = mockUserManagerState,
+        launcherApps: LauncherApps = mockLauncherApps,
+        shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mKeyToPinnedShortcutsMap,
+        app: LauncherAppState = mockAppState,
+        bgDataModel: BgDataModel = mockBgDataModel,
+        widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> = mWidgetProvidersMap,
+        widgetInflater: WidgetInflater = mockWidgetInflater,
+        pmHelper: PackageManagerHelper = mockPmHelper,
+        iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mIconRequestInfos,
+        isSdCardReady: Boolean = false,
+        pendingPackages: MutableSet<PackageUserKey> = mPendingPackages,
+        unlockedUsers: LongSparseArray<Boolean> = mUnlockedUsersArray,
+        installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = mInstallingPkgs,
+        allDeepShortcuts: MutableList<CacheableShortcutInfo> = mAllDeepShortcuts,
+    ) =
+        WorkspaceItemProcessor(
+            c = cursor,
+            memoryLogger = memoryLogger,
+            userCache = userCache,
+            userManagerState = userManagerState,
+            launcherApps = launcherApps,
+            app = app,
+            bgDataModel = bgDataModel,
+            widgetProvidersMap = widgetProvidersMap,
+            widgetInflater = widgetInflater,
+            pmHelper = pmHelper,
+            unlockedUsers = unlockedUsers,
+            iconRequestInfos = iconRequestInfos,
+            pendingPackages = pendingPackages,
+            isSdCardReady = isSdCardReady,
+            shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
+            installingPkgs = installingPkgs,
+            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 58b915f..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
@@ -94,7 +98,13 @@
 
         val srcCountMap = itemsToMap(srcGrid.items)
         val resultCountMap = itemsToMap(resultItems)
-        val diff = resultCountMap - srcCountMap
+        val diff = resultCountMap.toMutableMap()
+        for ((srcKey, srcValue) in srcCountMap) {
+            val destValue = diff[srcKey]
+            if (destValue != null) {
+                diff[srcKey] = destValue - srcValue
+            }
+        }
 
         diff.forEach { (k, count) ->
             assert(count >= 0) { "Source item $k not present on the result" }
@@ -108,17 +118,14 @@
         }
     }
 
-    private fun migrate(
-        srcGrid: Grid,
-        dstGrid: Grid,
-    ): List<WorkspaceItem> {
+    private fun migrate(srcGrid: Grid, dstGrid: Grid): List<WorkspaceItem> {
         val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
         val dbHelper =
             DatabaseHelper(
                 context,
                 null,
                 { UserCache.INSTANCE.get(context).getSerialNumberForUser(it) },
-                {}
+                {},
             )
 
         Favorites.addTableToDb(dbHelper.writableDatabase, userSerial, false, srcGrid.tableName)
@@ -127,22 +134,52 @@
         addItemsToDb(dbHelper.writableDatabase, dstGrid)
 
         LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use {
-            GridSizeMigrationUtil.migrate(
-                dbHelper,
-                GridSizeMigrationUtil.DbReader(it.db, srcGrid.tableName, context, MockSet(1)),
-                GridSizeMigrationUtil.DbReader(it.db, dstGrid.tableName, context, MockSet(1)),
-                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)
@@ -151,7 +188,7 @@
                 Grid(
                     tableName = Favorites.TMP_TABLE,
                     size = testCase.srcSize,
-                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER)
+                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER),
                 )
             val dstGrid =
                 Grid(tableName = Favorites.TABLE_NAME, size = testCase.targetSize, items = listOf())
@@ -160,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)
@@ -169,13 +217,13 @@
                 Grid(
                     tableName = Favorites.TMP_TABLE,
                     size = testCase.srcSize,
-                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER)
+                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER),
                 )
             val dstGrid =
                 Grid(
                     tableName = Favorites.TABLE_NAME,
                     size = testCase.targetSize,
-                    items = generateItemsForTest(testCase.destBoards, REPEAT_AFTER_DST)
+                    items = generateItemsForTest(testCase.destBoards, REPEAT_AFTER_DST),
                 )
             validate(srcGrid, dstGrid, migrate(srcGrid, dstGrid))
         }
@@ -184,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)
@@ -193,7 +254,7 @@
                 Grid(
                     tableName = Favorites.TMP_TABLE,
                     size = testCase.srcSize,
-                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER)
+                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER),
                 )
             val dstGrid =
                 Grid(tableName = Favorites.TABLE_NAME, size = testCase.targetSize, items = listOf())
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/pageindicators/PageIndicatorDotsTest.kt b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
new file mode 100644
index 0000000..9a8f957
--- /dev/null
+++ b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.pageindicators
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+
+class PageIndicatorDotsTest {
+
+    private val context: Context =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+    private val pageIndicatorDots: PageIndicatorDots = Mockito.spy(PageIndicatorDots(context))
+
+    @Test
+    fun `setActiveMarker should set the active page to the parameter passed`() {
+        pageIndicatorDots.setActiveMarker(2)
+
+        assertEquals(2, pageIndicatorDots.activePage)
+    }
+
+    @Test
+    fun `setActiveMarker should set the active page to the parameter passed divided by two in two panel layouts`() {
+        pageIndicatorDots.mIsTwoPanels = true
+
+        pageIndicatorDots.setActiveMarker(5)
+
+        assertEquals(2, pageIndicatorDots.activePage)
+    }
+
+    @Test
+    fun `setMarkersCount should set the number of pages to the passed parameter and if the last page gets removed we want to go to the previous page`() {
+        pageIndicatorDots.setMarkersCount(3)
+
+        assertEquals(3, pageIndicatorDots.numPages)
+    }
+
+    @Test
+    fun `for setMarkersCount if the last page gets removed we want to go to the previous page`() {
+        pageIndicatorDots.setActiveMarker(2)
+
+        pageIndicatorDots.setMarkersCount(2)
+
+        assertEquals(1, pageIndicatorDots.activePage)
+        assertEquals(pageIndicatorDots.activePage.toFloat(), pageIndicatorDots.currentPosition)
+    }
+}
diff --git a/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
new file mode 100644
index 0000000..ca2ef42
--- /dev/null
+++ b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -0,0 +1,266 @@
+/*
+ * 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.pm
+
+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
+import android.graphics.Bitmap
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.PROMISE_ICON_IDS
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionHelperTest {
+
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = spy(launcherModelHelper.sandboxContext)
+    private val packageManager = sandboxContext.packageManager
+    private val expectedAppPackage = "expectedAppPackage"
+    private val expectedInstallerPackage = "expectedInstallerPackage"
+    private val mockPackageInstaller: PackageInstaller = mock()
+
+    private lateinit var installSessionHelper: InstallSessionHelper
+    private lateinit var launcherApps: LauncherApps
+
+    @Before
+    fun setup() {
+        whenever(packageManager.packageInstaller).thenReturn(mockPackageInstaller)
+        whenever(sandboxContext.packageName).thenReturn(expectedInstallerPackage)
+        launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+        installSessionHelper = InstallSessionHelper(sandboxContext)
+    }
+
+    @Test
+    fun `getActiveSessions fetches verified install sessions from LauncherApps`() {
+        // Given
+        val expectedVerifiedSession1 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 0
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        val expectedVerifiedSession2 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+        // When
+        val actualSessions = installSessionHelper.getActiveSessions()
+        // Then
+        assertThat(actualSessions.values.toList()).isEqualTo(expectedSessions)
+    }
+
+    @Test
+    fun `getActiveSessionInfo fetches verified install sessions for given user and pkg`() {
+        // Given
+        val expectedVerifiedSession =
+            PackageInstaller.SessionInfo().apply {
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions)
+            .thenReturn(listOf(expectedVerifiedSession))
+        // When
+        val actualSession =
+            installSessionHelper.getActiveSessionInfo(UserHandle(0), expectedAppPackage)
+        // Then
+        assertThat(actualSession).isEqualTo(expectedVerifiedSession)
+    }
+
+    @Test
+    fun `getVerifiedSessionInfo verifies and returns session for given id`() {
+        // Given
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        whenever(mockPackageInstaller.getSessionInfo(1)).thenReturn(expectedSession)
+        // When
+        val actualSession = installSessionHelper.getVerifiedSessionInfo(1)
+        // Then
+        assertThat(actualSession).isEqualTo(expectedSession)
+    }
+
+    @Test
+    fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
+        // Given
+        val expectedApplicationInfo =
+            ApplicationInfo().apply { flags = FLAG_SYSTEM or FLAG_INSTALLED }
+        doReturn(expectedApplicationInfo)
+            .whenever(launcherApps)
+            .getApplicationInfo(eq(expectedAppPackage), any(), eq(UserHandle(0)))
+        // When
+        val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+
+    @Test
+    fun `getAllVerifiedSessions verifies and returns all active install sessions`() {
+        // Given
+        val expectedVerifiedSession1 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 0
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        val expectedVerifiedSession2 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+        // When
+        val actualSessions = installSessionHelper.allVerifiedSessions
+        // Then
+        assertThat(actualSessions).isEqualTo(expectedSessions)
+    }
+
+    @Test
+    fun `promiseIconAddedForId returns true if there is a promiseIcon with the session id`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = false
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            actualResult = installSessionHelper.promiseIconAddedForId(1)
+        }
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+
+    @Test
+    fun `removePromiseIconId removes promiseIconId for given Session id`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = true
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            installSessionHelper.removePromiseIconId(1)
+            actualResult = installSessionHelper.promiseIconAddedForId(1)
+        }
+        // Then
+        assertThat(actualResult).isFalse()
+    }
+
+    @Test
+    fun `tryQueuePromiseAppIcon will update promise icon ids from eligible sessions`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "appPackage"
+                userId = 0
+                appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+                appLabel = "appLabel"
+                installReason = PackageManager.INSTALL_REASON_USER
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var wasPromiseIconAdded = false
+        var actualPromiseIconIds = ""
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            installSessionHelper.removePromiseIconId(1)
+            installSessionHelper.tryQueuePromiseAppIcon(expectedSession)
+            wasPromiseIconAdded = installSessionHelper.promiseIconAddedForId(1)
+            actualPromiseIconIds = LauncherPrefs.get(sandboxContext).get(PROMISE_ICON_IDS)
+        }
+        // Then
+        assertThat(wasPromiseIconAdded).isTrue()
+        assertThat(actualPromiseIconIds).isEqualTo(expectedIdString)
+    }
+
+    @Test
+    fun `verifySessionInfo is true if can verify given SessionInfo`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "appPackage"
+                userId = 0
+                appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+                appLabel = "appLabel"
+                installReason = PackageManager.INSTALL_REASON_USER
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = false
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            actualResult = installSessionHelper.verifySessionInfo(expectedSession)
+        }
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+}
diff --git a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
index 98b6b4b..ae54e95 100644
--- a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
+++ b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
@@ -19,19 +19,26 @@
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
+import static com.android.launcher3.Flags.FLAG_ENABLE_DISMISS_PREDICTION_UNDO;
 import static com.android.launcher3.Flags.FLAG_ENABLE_PRIVATE_SPACE;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP;
 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 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.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -50,8 +57,15 @@
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
+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;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -61,8 +75,14 @@
 import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext;
 import com.android.launcher3.util.LauncherMultivalentJUnit;
 import com.android.launcher3.util.TestSandboxModelContextWrapper;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.UserIconInfo;
-import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.views.Snackbar;
+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;
@@ -70,11 +90,10 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
-
 @SmallTest
 @RunWith(LauncherMultivalentJUnit.class)
 public class SystemShortcutTest {
@@ -86,27 +105,35 @@
     private TestSandboxModelContextWrapper mTestContext;
     private final SandboxModelContext mSandboxContext = new SandboxModelContext();
     private PrivateProfileManager mPrivateProfileManager;
-    private PopupDataProvider mPopupDataProvider;
+    private WidgetPickerDataProvider mWidgetPickerDataProvider;
     private AppInfo mAppInfo;
+
     @Mock UserCache mUserCache;
     @Mock ApiWrapper mApiWrapper;
-    @Mock BaseDragLayer mBaseDragLayer;
     @Mock UserIconInfo mUserIconInfo;
     @Mock LauncherActivityInfo mLauncherActivityInfo;
     @Mock ApplicationInfo mApplicationInfo;
     @Mock Intent mIntent;
+    @Mock StatsLogManager mStatsLogManager;
+    @Mock(answer = Answers.RETURNS_SELF) StatsLogger mStatsLogger;
 
     @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);
-        mView = new View(mSandboxContext);
-        spyOn(mTestContext);
+        mTestContext = new TestSandboxModelContextWrapper(mSandboxContext) {
+            @Override
+            public StatsLogManager getStatsLogManager() {
+                return mStatsLogManager;
+            }
+        };
         spyOn(mSandboxContext);
-        doReturn(mBaseDragLayer).when(mTestContext).getDragLayer();
+        doReturn(mStatsLogger).when(mStatsLogManager).logger();
 
+        mView = new View(mTestContext);
         mItemInfo = new ItemInfo();
 
         LauncherApps mLauncherApps = mSandboxContext.spyService(LauncherApps.class);
@@ -114,13 +141,12 @@
         when(mLauncherActivityInfo.getApplicationInfo()).thenReturn(mApplicationInfo);
 
         when(mUserCache.getUserInfo(any())).thenReturn(mUserIconInfo);
-        when(mBaseDragLayer.getChildCount()).thenReturn(0);
         mPrivateProfileManager = mTestContext.getAppsView().getPrivateProfileManager();
         spyOn(mPrivateProfileManager);
         when(mPrivateProfileManager.getProfileUser()).thenReturn(PRIVATE_HANDLE);
 
-        mPopupDataProvider = mTestContext.getPopupDataProvider();
-        spyOn(mPopupDataProvider);
+        mWidgetPickerDataProvider = mTestContext.getWidgetPickerDataProvider();
+        spyOn(mWidgetPickerDataProvider);
     }
 
     @After
@@ -141,7 +167,7 @@
         mAppInfo = new AppInfo();
         mAppInfo.componentName = new ComponentName(mTestContext, getClass());
         assertNotNull(mAppInfo.getTargetComponent());
-        doReturn(new ArrayList<>()).when(mPopupDataProvider).getWidgetsForPackageUser(any());
+        doReturn(new WidgetPickerData()).when(mWidgetPickerDataProvider).get();
         spyOn(mAppInfo);
         SystemShortcut systemShortcut = SystemShortcut.WIDGETS
                 .getShortcut(mTestContext, mAppInfo, mView);
@@ -168,6 +194,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_ENABLE_DISMISS_PREDICTION_UNDO)
     public void testDontSuggestAppForPredictedItem() {
         mAppInfo = new AppInfo();
         mAppInfo.componentName = new ComponentName(mTestContext, getClass());
@@ -176,7 +203,36 @@
         SystemShortcut systemShortcut = SystemShortcut.DONT_SUGGEST_APP
                 .getShortcut(mTestContext, mAppInfo, mView);
         assertNotNull(systemShortcut);
-        systemShortcut.onClick(mView);
+
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () -> systemShortcut.onClick(mView));
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(mStatsLogger).log(eq(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP));
+        assertFalse(AbstractFloatingView.hasOpenView(mTestContext, TYPE_SNACKBAR));
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DISMISS_PREDICTION_UNDO)
+    public void testDontSuggestAppForPredictedItemWithUndo() {
+        mAppInfo = new AppInfo();
+        mAppInfo.componentName = new ComponentName(mTestContext, getClass());
+        mAppInfo.container = CONTAINER_HOTSEAT_PREDICTION;
+        assertTrue(mAppInfo.isPredictedItem());
+        SystemShortcut systemShortcut = SystemShortcut.DONT_SUGGEST_APP
+                .getShortcut(mTestContext, mAppInfo, mView);
+        assertNotNull(systemShortcut);
+
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () -> systemShortcut.onClick(mView));
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        verify(mStatsLogger).log(eq(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP));
+
+        // Undo bar shown
+        Snackbar snackbar = AbstractFloatingView.getOpenView(mTestContext, TYPE_SNACKBAR);
+        assertNotNull(snackbar);
+        reset(mStatsLogger);
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, snackbar.findViewById(
+                R.id.action)::performClick);
+        verify(mStatsLogger).log(eq(LAUNCHER_DISMISS_PREDICTION_UNDO));
     }
 
     @Test
@@ -356,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/tablet/TaplIsTabletTest.kt b/tests/src/com/android/launcher3/tablet/TaplIsTabletTest.kt
new file mode 100644
index 0000000..a6de607
--- /dev/null
+++ b/tests/src/com/android/launcher3/tablet/TaplIsTabletTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.tablet
+
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import com.android.launcher3.Launcher
+import com.android.launcher3.ui.AbstractLauncherUiTest
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import org.junit.Test
+
+class TaplIsTabletTest : AbstractLauncherUiTest<Launcher>() {
+
+    /** Investigating b/366237798 by isolating and seeing flake rate of mLauncher.isTablet */
+    @Test
+    @AllowedDevices(
+        DeviceProduct.CF_FOLDABLE,
+        DeviceProduct.CF_TABLET,
+        DeviceProduct.TANGORPRO,
+        DeviceProduct.FELIX,
+        DeviceProduct.COMET,
+    )
+    fun isTabletShouldBeTrue() {
+        assertTrue(mLauncher.isTablet)
+    }
+
+    /** Investigating b/366237798 by isolating and seeing flake rate of mLauncher.isTablet */
+    @Test
+    @AllowedDevices(DeviceProduct.CF_PHONE, DeviceProduct.CHEETAH)
+    fun isTabletShouldBeFalse() {
+        assertFalse(mLauncher.isTablet)
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 3d253b4..a273648 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -20,21 +20,18 @@
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
-import static com.android.launcher3.testing.shared.TestProtocol.WIDGET_CONFIG_NULL_EXTRA_INTENT;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.TestUtil.resolveSystemAppInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.graphics.Point;
@@ -44,6 +41,7 @@
 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;
 
@@ -64,7 +62,6 @@
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule;
@@ -98,12 +95,12 @@
  */
 public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> {
 
-    public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
     public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 10;
 
-    public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT;
     private static final String TAG = "AbstractLauncherUiTest";
 
+    private static final long BYTES_PER_MEGABYTE = 1 << 20;
+
     private static boolean sDumpWasGenerated = false;
     private static boolean sActivityLeakReported = false;
     private static boolean sSeenKeyguard = false;
@@ -125,6 +122,10 @@
     protected String mTargetPackage;
     private int mLauncherPid;
 
+    private final ActivityManager.MemoryInfo mMemoryInfo = new ActivityManager.MemoryInfo();
+    private final ActivityManager mActivityManager;
+    private long mMemoryBefore;
+
     /** Detects activity leaks and throws an exception if a leak is found. */
     public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
         checkDetectedLeaks(launcher, false);
@@ -146,7 +147,7 @@
                     launcher.forceGc();
                     return MAIN_EXECUTOR.submit(
                             () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
-                }, DEFAULT_UI_TIMEOUT, launcher);
+                }, launcher);
     }
 
     public static String getAppPackageName() {
@@ -193,6 +194,8 @@
     }
 
     protected AbstractLauncherUiTest() {
+        mActivityManager = InstrumentationRegistry.getContext()
+                .getSystemService(ActivityManager.class);
         mLauncher.enableCheckEventsForSuccessfulGestures();
         mLauncher.setAnomalyChecker(AbstractLauncherUiTest::verifyKeyguardInvisible);
         try {
@@ -224,6 +227,9 @@
     @Rule
     public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
 
+    @Rule
+    public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
+
     public static void initialize(AbstractLauncherUiTest test) throws Exception {
         test.reinitializeLauncherData();
         test.mDevice.pressHome();
@@ -237,21 +243,17 @@
     }
 
     protected void clearPackageData(String pkg) throws IOException, InterruptedException {
-        final CountDownLatch count = new CountDownLatch(2);
-        final SimpleBroadcastReceiver broadcastReceiver =
-                new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> count.countDown());
-        // We OK to make binder calls on main thread in test.
-        broadcastReceiver.registerPkgActions(mTargetContext, pkg,
-                Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED);
-
-        mDevice.executeShellCommand("pm clear " + pkg);
-        assertTrue(pkg + " didn't restart", count.await(10, TimeUnit.SECONDS));
-        mTargetContext.unregisterReceiver(broadcastReceiver);
+        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 ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
-                Launcher.ACTIVITY_TRACKER::getCreatedActivity);
+                Launcher.ACTIVITY_TRACKER::getCreatedContext);
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner<LAUNCHER_TYPE>(this))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
@@ -313,6 +315,26 @@
         initialize(this);
     }
 
+    private long getAvailableMemory() {
+        mActivityManager.getMemoryInfo(mMemoryInfo);
+
+        return Math.divideExact(mMemoryInfo.availMem,  BYTES_PER_MEGABYTE);
+    }
+
+    @Before
+    public void saveMemoryBefore() {
+        mMemoryBefore = getAvailableMemory();
+    }
+
+    @After
+    public void logMemoryAfter() {
+        long memoryAfter = getAvailableMemory();
+
+        Log.d(TAG, "Available memory: before=" + mMemoryBefore
+                + "MB, after=" + memoryAfter
+                + "MB, delta=" + (memoryAfter - mMemoryBefore) + "MB");
+    }
+
     /** Method that should be called when a test starts. */
     public static void onTestStart() {
         waitForSetupWizardDismissal();
@@ -353,8 +375,6 @@
 
     /** Waits for setup wizard to go away. */
     private static void waitForSetupWizardDismissal() {
-        if (!TestStabilityRule.isPresubmit()) return;
-
         if (sFirstTimeWaitingForWizard) {
             try {
                 getUiDevice().executeShellCommand(
@@ -390,6 +410,7 @@
     public void verifyLauncherState() {
         try {
             // Limits UI tests affecting tests running after them.
+            mDevice.pressHome();
             mLauncher.waitForLauncherInitialized();
             if (mLauncherPid != 0) {
                 assertEquals("Launcher crashed, pid mismatch:",
@@ -418,7 +439,7 @@
      */
     protected <T> T getOnUiThread(final Callable<T> callback) {
         try {
-            return mMainThreadExecutor.submit(callback).get(DEFAULT_UI_TIMEOUT,
+            return mMainThreadExecutor.submit(callback).get(TestUtil.DEFAULT_UI_TIMEOUT,
                     TimeUnit.MILLISECONDS);
         } catch (TimeoutException e) {
             Log.e(TAG, "Timeout in getOnUiThread, sending SIGABRT", e);
@@ -431,7 +452,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) {
@@ -473,13 +494,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
@@ -488,12 +503,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];
@@ -501,7 +516,7 @@
             final Object fromLauncher = getFromLauncher(f);
             output[0] = fromLauncher;
             return fromLauncher != null;
-        }, timeout, mLauncher);
+        }, mLauncher);
         return (T) output[0];
     }
 
@@ -515,12 +530,7 @@
         Wait.atMost(message, () -> {
             testThreadAction.run();
             return getFromLauncher(condition);
-        }, timeout, mLauncher);
-    }
-
-    protected LauncherActivityInfo getSettingsApp() {
-        return mTargetContext.getSystemService(LauncherApps.class)
-                .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
+        }, mLauncher, timeout);
     }
 
     /**
@@ -538,23 +548,13 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT, intent == null
-                    ? "AbstractLauncherUiTest.onReceive(): inputted intent NULL"
-                    : "AbstractLauncherUiTest.onReceive(): inputted intent NOT NULL");
             mIntent = intent;
             latch.countDown();
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT,
-                    "AbstractLauncherUiTest.onReceive() Countdown Latch started");
         }
 
         public Intent blockingGetIntent() throws InterruptedException {
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT,
-                    "AbstractLauncherUiTest.blockingGetIntent()");
             assertTrue("Timed Out", latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS));
             mTargetContext.unregisterReceiver(this);
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT, mIntent == null
-                    ? "AbstractLauncherUiTest.onReceive(): mIntent NULL"
-                    : "AbstractLauncherUiTest.onReceive(): mIntent NOT NULL");
             return mIntent;
         }
 
@@ -618,20 +618,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);
     }
 
 
@@ -647,8 +640,7 @@
                 launcher.finish();
             }
         });
-        waitForLauncherCondition(
-                "Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT);
+        waitForLauncherCondition("Launcher still active", launcher -> launcher == null);
     }
 
     protected boolean isInLaunchedApp(LAUNCHER_TYPE launcher) {
diff --git a/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
new file mode 100644
index 0000000..1cee71c
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.ui
+
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.views.DoubleShadowIconDrawable
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DoubleShadowIconDrawableTest {
+
+    @Test
+    fun `DoubleShadowIconDrawable is setup correctly from given ShadowInfo`() {
+        // Given
+        val shadowInfo: ShadowInfo = mock()
+        val originalDrawable: Drawable = mock()
+        val iconSize = 2
+        val iconInsetSize = 1
+        // When
+        val drawableUnderTest =
+            DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize)
+        // Then
+        assertThat(drawableUnderTest.intrinsicHeight).isEqualTo(iconSize)
+        assertThat(drawableUnderTest.intrinsicWidth).isEqualTo(iconSize)
+    }
+
+    @Test
+    fun `createShadowRenderNode creates RenderNode for shadow effects`() {
+        // Given
+        val shadowInfo =
+            ShadowInfo(
+                ambientShadowBlur = 1f,
+                ambientShadowColor = 2,
+                keyShadowBlur = 3f,
+                keyShadowOffsetX = 4f,
+                keyShadowOffsetY = 5f,
+                keyShadowColor = 6
+            )
+        val originalDrawable: Drawable = mock()
+        val iconSize = 2
+        val iconInsetSize = 1
+        // When
+        val shadowDrawableUnderTest =
+            spy(DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize))
+        shadowDrawableUnderTest.createShadowRenderNode()
+        // Then
+        verify(shadowDrawableUnderTest)
+            .createShadowRenderEffect(
+                shadowInfo.ambientShadowBlur,
+                0f,
+                0f,
+                Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+            )
+        verify(shadowDrawableUnderTest)
+            .createShadowRenderEffect(
+                shadowInfo.keyShadowBlur,
+                shadowInfo.keyShadowOffsetX,
+                shadowInfo.keyShadowOffsetY,
+                Color.alpha(shadowInfo.keyShadowColor).toFloat()
+            )
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
new file mode 100644
index 0000000..ef4dc1a
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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 android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadowInfoTest {
+
+    @Test
+    fun `ShadowInfo is created correctly from context`() {
+        // Given
+        val mockContext: Context = mock()
+        val mockAttrs: AttributeSet = mock()
+        val styledAttrs: TypedArray = mock()
+        val expectedShadowInfo =
+            ShadowInfo(
+                ambientShadowBlur = 1f,
+                ambientShadowColor = 2,
+                keyShadowBlur = 3f,
+                keyShadowOffsetX = 4f,
+                keyShadowOffsetY = 5f,
+                keyShadowColor = 6
+            )
+        doReturn(styledAttrs)
+            .whenever(mockContext)
+            .obtainStyledAttributes(mockAttrs, R.styleable.ShadowInfo, 0, 0)
+        doReturn(1)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+        doReturn(2).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_ambientShadowColor, 0)
+        doReturn(3)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+        doReturn(4)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+        doReturn(5)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+        doReturn(6).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+        // When
+        val actualShadowInfo = ShadowInfo.fromContext(mockContext, mockAttrs, 0)
+        // Then
+        assertThat(actualShadowInfo.ambientShadowBlur).isEqualTo(1)
+        assertThat(actualShadowInfo.ambientShadowColor).isEqualTo(2)
+        assertThat(actualShadowInfo.keyShadowBlur).isEqualTo(3)
+        assertThat(actualShadowInfo.keyShadowOffsetX).isEqualTo(4)
+        assertThat(actualShadowInfo.keyShadowOffsetY).isEqualTo(5)
+        assertThat(actualShadowInfo.keyShadowColor).isEqualTo(6)
+        assertThat(actualShadowInfo).isEqualTo(expectedShadowInfo)
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
similarity index 75%
rename from tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
rename to tests/src/com/android/launcher3/ui/WorkProfileTest.java
index b2e413d..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,38 +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.ScreenRecord;
+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+");
@@ -90,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() {
@@ -129,7 +107,7 @@
                 return true;
             }
             return false;
-        }, LauncherInstrumentation.WAIT_TIME_MS);
+        }, WAIT_TIME_MS);
     }
 
     @Test
@@ -139,15 +117,14 @@
         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
     @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
-    @ScreenRecord
     @Test
     public void toggleWorks() {
         assumeTrue(mWorkProfileSetupSuccessful);
@@ -159,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);
 
@@ -191,11 +167,10 @@
         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);
 
     }
 
-    @ScreenRecord // b/322823478
     @Test
     public void testEdu() {
         assumeTrue(mWorkProfileSetupSuccessful);
@@ -219,7 +194,7 @@
             } finally {
                 l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
             }
-        }, LauncherInstrumentation.WAIT_TIME_MS);
+        }, WAIT_TIME_MS);
     }
 
     private void updateWorkProfileSetupSuccessful(String cli, String output) {
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
index e6e02b4..7845222 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
@@ -93,7 +93,7 @@
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
         mLauncher.getWorkspace()
                 .openAllWidgets()
-                .getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))
+                .getWidget(mWidgetInfo.getLabel())
                 .dragToWorkspace(true, false);
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
@@ -103,12 +103,12 @@
 
         setResultAndWaitForAnimation(acceptConfig);
         if (acceptConfig) {
-            Wait.atMost("", new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+            Wait.atMost("", new WidgetSearchCondition(), mLauncher);
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
             // Verify that the widget id is deleted.
             Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
-                    DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+                    mLauncher);
         }
     }
 
@@ -136,7 +136,7 @@
         @Override
         public boolean isTrue() throws Throwable {
             return mMainThreadExecutor.submit(() -> {
-                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
                 return l != null && l.getWorkspace().getFirstMatch(this) != null;
             }).get();
         }
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index 9b184ae..460ffc4 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -15,9 +15,6 @@
  */
 package com.android.launcher3.ui.widget;
 
-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 android.platform.test.annotations.PlatinumTest;
@@ -32,8 +29,8 @@
 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.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import org.junit.Assume;
@@ -65,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
@@ -102,7 +99,6 @@
     /**
      * Test dragging a widget to the workspace and resize it.
      */
-    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/316910614
     @PlatinumTest(focusArea = "launcher")
     @Test
     public void testResizeWidget() throws Throwable {
@@ -116,7 +112,7 @@
         WidgetResizeFrame resizeFrame = mLauncher
                 .getWorkspace()
                 .openAllWidgets()
-                .getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager()))
+                .getWidget(widgetInfo.getLabel())
                 .dragWidgetToWorkspace();
 
         assertNotNull("Widget resize frame not shown after widget add", resizeFrame);
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
index d40d3bc..4cdbd96 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
@@ -53,6 +53,7 @@
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
@@ -233,13 +234,15 @@
     }
 
     private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
-        final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT);
+        final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label,
+                TestUtil.DEFAULT_UI_TIMEOUT);
         assertTrue("Widget is not present",
                 widget != null);
     }
 
     private void verifyPendingWidgetPresent() {
-        final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT);
+        final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(
+                TestUtil.DEFAULT_UI_TIMEOUT);
         assertTrue("Pending widget is not present",
                 widget != null);
     }
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
index 74047f0..fe3b2ee 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
@@ -169,8 +169,7 @@
 
         // Go back to home
         mLauncher.goHome();
-        Wait.atMost("", new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
-                mLauncher);
+        Wait.atMost("", new ItemSearchCondition(itemMatcher), mLauncher);
     }
 
     /**
@@ -187,7 +186,7 @@
         @Override
         public boolean isTrue() throws Throwable {
             return mMainThreadExecutor.submit(() -> {
-                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
                 return l != null && l.getWorkspace().getFirstMatch(mOp) != null;
             }).get();
         }
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
deleted file mode 100644
index a148744..0000000
--- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.ui.workspace;
-
-import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.net.Uri;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.test.filters.LargeTest;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.icons.ThemedIconDrawable;
-import com.android.launcher3.tapl.HomeAllApps;
-import com.android.launcher3.tapl.HomeAppIcon;
-import com.android.launcher3.tapl.HomeAppIconMenuItem;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.rule.ScreenRecordRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
-
-import org.junit.Test;
-
-import java.util.ArrayDeque;
-import java.util.Queue;
-
-/**
- * Tests for theme icon support in Launcher
- *
- * Note running these tests will clear the workspace on the device.
- */
-@LargeTest
-public class TaplThemeIconsTest extends AbstractLauncherUiTest<Launcher> {
-
-    private static final String APP_NAME = "IconThemedActivity";
-    private static final String SHORTCUT_NAME = "Shortcut 1";
-
-    @Test
-    public void testIconWithoutTheme() throws Exception {
-        setThemeEnabled(false);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(APP_NAME);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getAppsView(), false));
-            icon.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), false));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testShortcutIconWithoutTheme() throws Exception {
-        setThemeEnabled(false);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(TEST_APP_NAME);
-            HomeAppIconMenuItem shortcutItem =
-                    (HomeAppIconMenuItem) icon.openDeepShortcutMenu().getMenuItem(SHORTCUT_NAME);
-            shortcutItem.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), false));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testIconWithTheme() throws Exception {
-        setThemeEnabled(true);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(APP_NAME);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getAppsView(), false));
-            icon.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), true));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/350557998
-    @ScreenRecordRule.ScreenRecord // b/350557998
-    public void testShortcutIconWithTheme() throws Exception {
-        setThemeEnabled(true);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(TEST_APP_NAME);
-            HomeAppIconMenuItem shortcutItem =
-                    (HomeAppIconMenuItem) icon.openDeepShortcutMenu().getMenuItem(SHORTCUT_NAME);
-            shortcutItem.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), true));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    private void verifyIconTheme(String title, ViewGroup parent, boolean isThemed) {
-        // Wait for Launcher model to be completed
-        try {
-            Executors.MODEL_EXECUTOR.submit(() -> { }).get();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-
-        // Find the app icon
-        Queue<View> viewQueue = new ArrayDeque<>();
-        viewQueue.add(parent);
-        BubbleTextView icon = null;
-        while (!viewQueue.isEmpty()) {
-            View view = viewQueue.poll();
-            if (view instanceof ViewGroup) {
-                parent = (ViewGroup) view;
-                for (int i = parent.getChildCount() - 1; i >= 0; i--) {
-                    viewQueue.add(parent.getChildAt(i));
-                }
-            } else if (view instanceof BubbleTextView btv) {
-                if (title.equals(btv.getContentDescription().toString())) {
-                    icon = btv;
-                    break;
-                }
-            }
-        }
-
-        assertNotNull(icon.getIcon());
-        assertEquals(isThemed, icon.getIcon() instanceof ThemedIconDrawable);
-    }
-
-    private void setThemeEnabled(boolean isEnabled) throws Exception {
-        Uri uri = new Uri.Builder()
-                .scheme(ContentResolver.SCHEME_CONTENT)
-                .authority(mTargetPackage + ".grid_control")
-                .appendPath("set_icon_themed")
-                .build();
-        ContentValues values = new ContentValues();
-        values.put("boolean_value", isEnabled);
-        try (ContentProviderClient client = mTargetContext.getContentResolver()
-                .acquireContentProviderClient(uri)) {
-            int result = client.update(uri, values, null);
-            assertTrue(result > 0);
-        }
-    }
-}
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index eb05000..2edd129 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -20,8 +20,6 @@
 import static com.android.launcher3.util.TestConstants.AppNames.MAPS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.MESSAGES_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.STORE_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;
@@ -40,7 +38,6 @@
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ScreenRecordRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.After;
 import org.junit.Before;
@@ -167,7 +164,6 @@
 
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/352130094
     public void testDragIconToPage2() {
         Workspace workspace = mLauncher.getWorkspace();
 
@@ -245,7 +241,6 @@
         });
     }
 
-    @ScreenRecordRule.ScreenRecord // b/329935119
     @Test
     @PortraitLandscape
     public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() {
@@ -288,6 +283,7 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/330232490
     public void testEmptyPagesGetRemovedIfBothPagesAreEmpty() {
         Workspace workspace = mLauncher.getWorkspace();
 
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
index 490cff2..237f2a9 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
@@ -32,7 +32,6 @@
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -50,12 +49,6 @@
         return launcher.getWorkspace().getCurrentPage();
     }
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     @After
     public void tearDown() throws Exception {
         if (mLauncherLayout != null) {
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
new file mode 100644
index 0000000..cfc0a6b
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.ui.workspace;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.uiautomator.UiDevice;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.allapps.AllAppsRecyclerView;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.icons.ThemedIconDrawable;
+import com.android.launcher3.popup.ArrowPopup;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.TestUtil;
+
+import org.junit.Test;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * Tests for theme icon support in Launcher
+ *
+ * Note running these tests will clear the workspace on the device.
+ */
+@LargeTest
+public class ThemeIconsTest extends BaseLauncherActivityTest<Launcher> {
+
+    private static final String APP_NAME = "IconThemedActivity";
+    private static final String SHORTCUT_NAME = "Shortcut 1";
+
+    @Test
+    public void testIconWithoutTheme() throws Exception {
+        setThemeEnabled(false);
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(APP_NAME);
+        BubbleTextView btv = getFromLauncher(
+                l -> verifyIconTheme(APP_NAME, l.getAppsView(), false));
+        addToWorkspace(btv);
+        executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), false));
+    }
+
+    @Test
+    public void testShortcutIconWithoutTheme() throws Exception {
+        setThemeEnabled(false);
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(TEST_APP_NAME);
+        BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, btv::performLongClick);
+
+        BubbleTextView menuItem = getOnceNotNull("Popup menu not open", l ->
+                (AbstractFloatingView.getOpenView(l, TYPE_ACTION_POPUP) instanceof ArrowPopup ap)
+                        ? findBtv(SHORTCUT_NAME, ap) : null);
+        addToWorkspace(menuItem);
+        executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), false));
+    }
+
+    @Test
+    public void testIconWithTheme() throws Exception {
+        setThemeEnabled(true);
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(APP_NAME);
+        BubbleTextView btv = getFromLauncher(l ->
+                verifyIconTheme(APP_NAME, l.getAppsView(), false));
+        addToWorkspace(btv);
+        executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), true));
+    }
+
+    @Test
+    public void testShortcutIconWithTheme() throws Exception {
+        setThemeEnabled(true);
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(TEST_APP_NAME);
+        BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, btv::performLongClick);
+
+        BubbleTextView menuItem = getOnceNotNull("Popup menu not open", l ->
+                (AbstractFloatingView.getOpenView(l, TYPE_ACTION_POPUP) instanceof ArrowPopup ap)
+                        ? findBtv(SHORTCUT_NAME, ap) : null);
+        addToWorkspace(menuItem);
+        executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), true));
+    }
+
+    private BubbleTextView findBtv(String title, ViewGroup parent) {
+        // Wait for Launcher model to be completed
+        try {
+            Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        // Find the app icon
+        Queue<View> viewQueue = new ArrayDeque<>();
+        viewQueue.add(parent);
+        BubbleTextView icon = null;
+        while (!viewQueue.isEmpty()) {
+            View view = viewQueue.poll();
+            if (view instanceof ViewGroup) {
+                parent = (ViewGroup) view;
+                for (int i = parent.getChildCount() - 1; i >= 0; i--) {
+                    viewQueue.add(parent.getChildAt(i));
+                }
+            } else if (view instanceof BubbleTextView btv) {
+                if (btv.getContentDescription() != null
+                        && title.equals(btv.getContentDescription().toString())) {
+                    icon = btv;
+                    break;
+                }
+            }
+        }
+        return icon;
+    }
+
+    private BubbleTextView verifyIconTheme(String title, ViewGroup parent, boolean isThemed) {
+        BubbleTextView icon = findBtv(title, parent);
+        assertNotNull(icon.getIcon());
+        assertEquals(isThemed, icon.getIcon() instanceof ThemedIconDrawable);
+        return icon;
+    }
+
+    private void setThemeEnabled(boolean isEnabled) throws Exception {
+        Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(targetContext().getPackageName() + ".grid_control")
+                .appendPath("set_icon_themed")
+                .build();
+        ContentValues values = new ContentValues();
+        values.put("boolean_value", isEnabled);
+        try (ContentProviderClient client = targetContext().getContentResolver()
+                .acquireContentProviderClient(uri)) {
+            int result = client.update(uri, values, null);
+            assertTrue(result > 0);
+        }
+    }
+
+    private void scrollToAppIcon(String appName) {
+        executeOnLauncher(l -> {
+            l.hideKeyboard();
+            AllAppsRecyclerView rv = l.getAppsView().getActiveRecyclerView();
+            int pos = rv.getApps().getAdapterItems().indexOf(rv.getApps().getAdapterItems().stream()
+                    .filter(i -> i.itemInfo != null && appName.equals(i.itemInfo.title.toString()))
+                    .findFirst()
+                    .get());
+            rv.getLayoutManager().scrollToPosition(pos);
+        });
+    }
+
+    private void addToWorkspace(View btv) {
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () ->
+                btv.getAccessibilityDelegate().performAccessibilityAction(
+                        btv, com.android.launcher3.R.id.action_add_to_workspace, null));
+        UiDevice.getInstance(getInstrumentation()).waitForIdle();
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
new file mode 100644
index 0000000..476e497
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util
+
+import android.content.Context
+import android.content.Intent
+import android.os.SystemClock
+import android.view.InputDevice
+import android.view.KeyCharacterMap
+import android.view.KeyEvent
+import android.view.MotionEvent
+import androidx.lifecycle.Lifecycle.State.RESUMED
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ActivityScenario.ActivityAction
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.Launcher
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherState
+import com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST
+import com.android.launcher3.tapl.TestHelpers
+import com.android.launcher3.util.ModelTestExtensions.loadModelSync
+import com.android.launcher3.util.Wait.atMost
+import java.util.function.Function
+import java.util.function.Supplier
+import org.junit.After
+
+/**
+ * Base class for tests which use Launcher activity with some utility methods.
+ *
+ * This should instead be a rule, but is kept as a base class for easier migration from TAPL
+ */
+open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> {
+
+    private var currentScenario: ActivityScenario<LAUNCHER_TYPE>? = null
+
+    val scenario: ActivityScenario<LAUNCHER_TYPE>
+        get() =
+            currentScenario
+                ?: ActivityScenario.launch<LAUNCHER_TYPE>(
+                        TestHelpers.getHomeIntentInPackage(targetContext()),
+                        null,
+                    )
+                    .also { currentScenario = it }
+
+    @After
+    fun closeCurrentActivity() {
+        currentScenario?.close()
+        currentScenario = null
+    }
+
+    protected fun loadLauncherSync() {
+        LauncherAppState.getInstance(targetContext()).model.loadModelSync()
+        scenario.moveToState(RESUMED)
+    }
+
+    protected fun targetContext(): Context = getInstrumentation().targetContext
+
+    protected fun goToState(state: LauncherState) {
+        executeOnLauncher { it.stateManager.goToState(state, 0) }
+        UiDevice.getInstance(getInstrumentation()).waitForIdle()
+    }
+
+    protected fun executeOnLauncher(f: ActivityAction<LAUNCHER_TYPE>) = scenario.onActivity(f)
+
+    protected fun <T> getFromLauncher(f: Function<in LAUNCHER_TYPE, out T?>): T? {
+        var result: T? = null
+        executeOnLauncher { result = f.apply(it) }
+        return result
+    }
+
+    protected fun isInState(state: Supplier<LauncherState>): Boolean =
+        getFromLauncher { it.stateManager.state == state.get() }!!
+
+    protected fun waitForState(message: String, state: Supplier<LauncherState>) =
+        waitForLauncherCondition(message) { it.stateManager.currentStableState === state.get() }
+
+    protected fun waitForLauncherCondition(
+        message: String,
+        condition: Function<LAUNCHER_TYPE, Boolean>,
+    ) = atMost(message, { getFromLauncher(condition)!! })
+
+    protected fun waitForLauncherCondition(
+        message: String,
+        condition: Function<LAUNCHER_TYPE, Boolean>,
+        timeout: Long,
+    ) = atMost(message, { getFromLauncher(condition)!! }, null, timeout)
+
+    protected fun <T> getOnceNotNull(message: String, f: Function<LAUNCHER_TYPE, T?>): T? {
+        var output: T? = null
+        atMost(
+            message,
+            {
+                val fromLauncher = getFromLauncher<T>(f)
+                output = fromLauncher
+                fromLauncher != null
+            },
+        )
+        return output
+    }
+
+    protected fun getAllAppsScroll(launcher: LAUNCHER_TYPE) =
+        launcher.appsView.activeRecyclerView.computeVerticalScrollOffset()
+
+    @JvmOverloads
+    protected fun injectKeyEvent(keyCode: Int, actionDown: Boolean, metaState: Int = 0) {
+        val eventTime = SystemClock.uptimeMillis()
+        val event =
+            KeyEvent.obtain(
+                eventTime,
+                eventTime,
+                if (actionDown) KeyEvent.ACTION_DOWN else MotionEvent.ACTION_UP,
+                keyCode,
+                /* repeat= */ 0,
+                metaState,
+                KeyCharacterMap.VIRTUAL_KEYBOARD,
+                /* scancode= */ 0,
+                /* flags= */ 0,
+                InputDevice.SOURCE_KEYBOARD,
+                /* characters =*/ null,
+            )
+        executeOnLauncher { it.dispatchKeyEvent(event) }
+        event.recycle()
+    }
+
+    fun startAppFast(packageName: String) {
+        val intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!
+        intent.addCategory(Intent.CATEGORY_LAUNCHER)
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        targetContext().startActivity(intent)
+        UiDevice.getInstance(getInstrumentation()).waitForIdle()
+    }
+
+    fun freezeAllApps() = executeOnLauncher {
+        it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST)
+    }
+
+    fun executeShellCommand(cmd: String) =
+        UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd)
+}
diff --git a/tests/src/com/android/launcher3/util/RoboApiWrapper.kt b/tests/src/com/android/launcher3/util/RoboApiWrapper.kt
new file mode 100644
index 0000000..7f74e56
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/RoboApiWrapper.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.util
+
+import android.content.ContentResolver
+import android.net.Uri
+import android.os.Looper
+import java.io.InputStream
+import java.util.function.Supplier
+
+object RoboApiWrapper {
+
+    fun registerInputStream(
+        contentResolver: ContentResolver,
+        uri: Uri,
+        inputStreamSupplier: Supplier<InputStream>,
+    ) {}
+
+    fun waitForLooperSync(looper: Looper) {}
+}
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
deleted file mode 100644
index 50bc32e..0000000
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.android.launcher3.util;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.launcher3.tapl.LauncherInstrumentation;
-
-import org.junit.Assert;
-
-import java.util.function.Supplier;
-
-/**
- * A utility class for waiting for a condition to be true.
- */
-public class Wait {
-
-    private static final long DEFAULT_SLEEP_MS = 200;
-
-    public static void atMost(String message, Condition condition, long timeout,
-            LauncherInstrumentation launcher) {
-        atMost(() -> message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
-    }
-
-    public static void atMost(Supplier<String> message, Condition condition, long timeout,
-            LauncherInstrumentation launcher) {
-        atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
-    }
-
-    public static void atMost(Supplier<String> message, Condition condition, long timeout,
-            long sleepMillis,
-            LauncherInstrumentation launcher) {
-        final long startTime = SystemClock.uptimeMillis();
-        long endTime = startTime + timeout;
-        Log.d("Wait", "atMost: " + startTime + " - " + endTime);
-        while (SystemClock.uptimeMillis() < endTime) {
-            try {
-                if (condition.isTrue()) {
-                    return;
-                }
-            } catch (Throwable t) {
-                throw new RuntimeException(t);
-            }
-            SystemClock.sleep(sleepMillis);
-        }
-
-        // Check once more before returning false.
-        try {
-            if (condition.isTrue()) {
-                return;
-            }
-        } catch (Throwable t) {
-            throw new RuntimeException(t);
-        }
-        Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
-        launcher.checkForAnomaly(false, false);
-        Assert.fail(message.get());
-    }
-
-    /**
-     * Interface representing a generic condition
-     */
-    public interface Condition {
-
-        boolean isTrue() throws Throwable;
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/Wait.kt b/tests/src/com/android/launcher3/util/Wait.kt
new file mode 100644
index 0000000..1e5af54
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/Wait.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util
+
+import android.os.SystemClock
+import android.util.Log
+import com.android.launcher3.tapl.LauncherInstrumentation
+import java.util.function.Supplier
+import org.junit.Assert
+
+/** A utility class for waiting for a condition to be true. */
+object Wait {
+    private const val DEFAULT_SLEEP_MS: Long = 200
+
+    @JvmStatic
+    @JvmOverloads
+    fun atMost(
+        message: String,
+        condition: Condition,
+        launcherInstrumentation: LauncherInstrumentation? = null,
+        timeout: Long = TestUtil.DEFAULT_UI_TIMEOUT,
+    ) {
+        atMost({ message }, condition, launcherInstrumentation, timeout)
+    }
+
+    @JvmStatic
+    @JvmOverloads
+    fun atMost(
+        message: Supplier<String>,
+        condition: Condition,
+        launcherInstrumentation: LauncherInstrumentation? = null,
+        timeout: Long = TestUtil.DEFAULT_UI_TIMEOUT,
+    ) {
+        val startTime = SystemClock.uptimeMillis()
+        val endTime = startTime + timeout
+        Log.d("Wait", "atMost: $startTime - $endTime")
+        while (SystemClock.uptimeMillis() < endTime) {
+            try {
+                if (condition.isTrue()) {
+                    return
+                }
+            } catch (t: Throwable) {
+                throw RuntimeException(t)
+            }
+            SystemClock.sleep(DEFAULT_SLEEP_MS)
+        }
+
+        // Check once more before returning false.
+        try {
+            if (condition.isTrue()) {
+                return
+            }
+        } catch (t: Throwable) {
+            throw RuntimeException(t)
+        }
+        Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis())
+        launcherInstrumentation?.checkForAnomaly(false, false)
+        Assert.fail(message.get())
+    }
+
+    /** Interface representing a generic condition */
+    fun interface Condition {
+
+        @Throws(Throwable::class) fun isTrue(): Boolean
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java b/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java
index 702988c..8a9ff3e 100644
--- a/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java
+++ b/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.util.rule;
 
+import static com.android.launcher3.util.TestUtil.grantWriteSecurePermission;
+
+import android.app.Instrumentation;
 import android.content.ContentResolver;
 import android.provider.Settings;
 import android.util.Log;
@@ -51,6 +54,7 @@
                 try {
                     Log.d(TAG, "In try-block: Setting long press timeout from "
                             + prevLongPressTimeout + "ms to " + newLongPressTimeout + "ms");
+                    grantWriteSecurePermission();
                     Settings.Secure.putInt(
                             contentResolver,
                             Settings.Secure.LONG_PRESS_TIMEOUT,
@@ -63,6 +67,7 @@
                 } finally {
                     Log.d(TAG, "In finally-block: resetting long press timeout to "
                             + prevLongPressTimeout + "ms");
+                    grantWriteSecurePermission();
                     Settings.Secure.putInt(
                             contentResolver,
                             Settings.Secure.LONG_PRESS_TIMEOUT,
diff --git a/tests/src_deviceless/com/android/launcher3/util/RoboApiWrapper.kt b/tests/src_deviceless/com/android/launcher3/util/RoboApiWrapper.kt
new file mode 100644
index 0000000..a2b8303
--- /dev/null
+++ b/tests/src_deviceless/com/android/launcher3/util/RoboApiWrapper.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.ContentResolver
+import android.net.Uri
+import android.os.Looper
+import java.io.InputStream
+import java.util.function.Supplier
+import org.robolectric.Shadows
+
+object RoboApiWrapper {
+
+    fun registerInputStream(
+        contentResolver: ContentResolver,
+        uri: Uri,
+        inputStreamSupplier: Supplier<InputStream>,
+    ) {
+        Shadows.shadowOf(contentResolver).registerInputStreamSupplier(uri, inputStreamSupplier)
+    }
+
+    fun waitForLooperSync(looper: Looper) {
+        Shadows.shadowOf(looper).runToEndOfTasks()
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 7c6d684..9294755 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,9 +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.util.Log;
+import android.graphics.Point;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -98,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");
     }
 
@@ -129,6 +126,14 @@
     }
 
     /**
+     * @return the center coordinates of the icon
+     */
+    @NonNull
+    public Point getVisibleCenter() {
+        return getObject().getVisibleCenter();
+    }
+
+    /**
      * Create a regular expression pattern that matches strings containing all of the non-whitespace
      * characters of the app name, with any amount of whitespace added between characters (e.g.
      * newline for multiline app labels).
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 988aa94..512db39 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.tapl.BaseOverview.TASK_RES_ID;
+import static com.android.launcher3.tapl.BaseOverview.TASK_SELECTOR;
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
 
@@ -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;
@@ -117,16 +118,35 @@
                         // non-tablet overview, snapshots can be on either side of the swiped
                         // task, but we still check that they become visible after swiping and
                         // pausing.
-                        mLauncher.waitForOverviewObject(TASK_RES_ID);
+                        mLauncher.waitForObjectBySelector(TASK_SELECTOR);
                         if (mLauncher.isTablet()) {
                             List<UiObject2> tasks = mLauncher.getDevice().findObjects(
-                                    mLauncher.getOverviewObjectSelector(TASK_RES_ID));
+                                    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 27f6c16..b15afc1 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -19,11 +19,13 @@
 import static android.view.KeyEvent.KEYCODE_ESCAPE;
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.tapl.LauncherInstrumentation.log;
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
+import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 
 import android.graphics.Rect;
-import android.util.Log;
 import android.view.KeyEvent;
 
 import androidx.annotation.NonNull;
@@ -35,9 +37,11 @@
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -45,7 +49,10 @@
  * Common overview panel for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
-    protected static final String TASK_RES_ID = "task";
+    private static final String TAG = "BaseOverview";
+    protected static final BySelector TASK_SELECTOR = By.res(Pattern.compile(
+            getOverviewPackageName()
+                    + ":id/(task_view_single|task_view_grouped|task_view_desktop)"));
     private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
     private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
@@ -55,10 +62,22 @@
 
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
 
+    private final @Nullable UiObject2 mLiveTileTask;
+
+
     BaseOverview(LauncherInstrumentation launcher) {
+        this(launcher, /*launchedFromApp=*/false);
+    }
+
+    BaseOverview(LauncherInstrumentation launcher, boolean launchedFromApp) {
         super(launcher);
         verifyActiveContainer();
         verifyActionsViewVisibility();
+        if (launchedFromApp) {
+            mLiveTileTask = getCurrentTaskUnchecked();
+        } else {
+            mLiveTileTask = null;
+        }
     }
 
     @Override
@@ -78,7 +97,7 @@
     private void flingForwardImpl() {
         try (LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling forward in overview")) {
-            LauncherInstrumentation.log("Overview.flingForward before fling");
+            log("Overview.flingForward before fling");
             final UiObject2 overview = verifyActiveContainer();
             final int leftMargin =
                     mLauncher.getTargetInsets().left + mLauncher.getEdgeSensitivityWidth();
@@ -104,7 +123,7 @@
     private void flingBackwardImpl() {
         try (LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling backward in overview")) {
-            LauncherInstrumentation.log("Overview.flingBackward before fling");
+            log("Overview.flingBackward before fling");
             final UiObject2 overview = verifyActiveContainer();
             final int rightMargin =
                     mLauncher.getTargetInsets().right + mLauncher.getEdgeSensitivityWidth();
@@ -259,7 +278,7 @@
                     if (mLauncher.isTablet()) {
                         mLauncher.assertTrue("current task is not grid height",
                                 getCurrentTask().getVisibleHeight() == mLauncher
-                                        .getGridTaskRectForTablet().height());
+                                        .getOverviewGridTaskSize().height());
                     }
                     mLauncher.assertTrue("Current task not scrolled off screen",
                             !getCurrentTask().equals(task));
@@ -275,37 +294,56 @@
      */
     @NonNull
     public OverviewTask getCurrentTask() {
+        UiObject2 currentTask = getCurrentTaskUnchecked();
+        mLauncher.assertNotNull("Unable to find a task", currentTask);
+        return new OverviewTask(mLauncher, currentTask, this);
+    }
+
+    @Nullable
+    private UiObject2 getCurrentTaskUnchecked() {
         final List<UiObject2> taskViews = getTasks();
-        mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
+        if (taskViews.isEmpty()) {
+            return null;
+        }
 
         // The widest, and most top-right task should be the current task
-        UiObject2 currentTask = Collections.max(taskViews,
+        return Collections.max(taskViews,
                 Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width())
                         .thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x)
                         .thenComparing(Comparator.comparing(
                                 (UiObject2 t) -> t.getVisibleCenter().y).reversed()));
-        return new OverviewTask(mLauncher, currentTask, this);
     }
 
-    /** Returns an overview task matching TestActivity {@param activityNumber}. */
+    /**
+     * Returns an overview task that contains the specified test activity in its thumbnails.
+     *
+     * @param activityIndex index of TestActivity to match against
+     */
     @NonNull
-    public OverviewTask getTestActivityTask(int activityNumber) {
+    public OverviewTask getTestActivityTask(int activityIndex) {
+        return getTestActivityTask(Collections.singleton(activityIndex));
+    }
+
+    /**
+     * Returns an overview task that contains all the specified test activities in its thumbnails.
+     *
+     * @param activityNumbers collection of indices of TestActivity to match against
+     */
+    @NonNull
+    public OverviewTask getTestActivityTask(Collection<Integer> activityNumbers) {
         final List<UiObject2> taskViews = getTasks();
         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
 
-        final String activityName = "TestActivity" + activityNumber;
-        UiObject2 task = null;
-        for (UiObject2 taskView : taskViews) {
-            // TODO(b/239452415): Use equals instead of descEndsWith
-            if (taskView.getParent().hasObject(By.descEndsWith(activityName))) {
-                task = taskView;
-                break;
-            }
-        }
-        mLauncher.assertNotNull(
-                "Unable to find a task with " + activityName + " from the task list", task);
+        Optional<UiObject2> task = taskViews.stream().filter(
+                taskView -> activityNumbers.stream().allMatch(activityNumber ->
+                    // TODO(b/239452415): Use equals instead of descEndsWith
+                    taskView.hasObject(By.descEndsWith("TestActivity" + activityNumber))
+                )).findFirst();
 
-        return new OverviewTask(mLauncher, task, this);
+        mLauncher.assertTrue("Unable to find a task with test activities " + activityNumbers
+                + " from the task list", task.isPresent());
+
+        return new OverviewTask(mLauncher, task.get(), this);
     }
 
     /**
@@ -316,7 +354,7 @@
         final List<UiObject2> taskViews = getTasks();
         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
 
-        final int gridTaskWidth = mLauncher.getGridTaskRectForTablet().width();
+        final int gridTaskWidth = mLauncher.getOverviewGridTaskSize().width();
 
         return taskViews.stream().filter(t -> t.getVisibleBounds().width() == gridTaskWidth).map(
                 t -> new OverviewTask(mLauncher, t, this)).collect(Collectors.toList());
@@ -327,12 +365,10 @@
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get overview tasks")) {
             verifyActiveContainer();
-            return mLauncher.getDevice().findObjects(
-                    mLauncher.getOverviewObjectSelector(TASK_RES_ID));
+            return mLauncher.getDevice().findObjects(TASK_SELECTOR);
         }
     }
 
-
     int getTaskCount() {
         return getTasks().size();
     }
@@ -384,39 +420,33 @@
     }
 
     protected boolean isActionsViewVisible() {
-        boolean hasTasks = hasTasks();
-        if (!hasTasks || isClearAllVisible()) {
-            LauncherInstrumentation.log("Not expecting an actions bar:"
-                    + (!hasTasks ? "no recent tasks" : "clear all button is visible"));
+        if (!hasTasks() || isClearAllVisible()) {
+            testLogD(TAG, "Not expecting an actions bar: no tasks/'Clear all' is visible");
             return false;
         }
         boolean isTablet = mLauncher.isTablet();
         if (isTablet && mLauncher.isGridOnlyOverviewEnabled()) {
-            LauncherInstrumentation.log("Not expecting an actions bar: "
-                    + "device is tablet with grid-only Overview");
+            testLogD(TAG, "Not expecting an actions bar: device is tablet with grid-only Overview");
             return false;
         }
         OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask();
         if (task == null) {
-            LauncherInstrumentation.log("Not expecting an actions bar: no focused task");
+            testLogD(TAG, "Not expecting an actions bar: no current task");
             return false;
         }
-        float centerOffset = Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX());
         // In tablets, if focused task is not in center, overview actions aren't visible.
-        if (isTablet && centerOffset >= 1) {
-            LauncherInstrumentation.log("Not expecting an actions bar: "
-                    + "device is tablet and task is not centered; center offset by "
-                    + centerOffset + "px");
+        if (isTablet && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) {
+            testLogD(TAG,
+                    "Not expecting an actions bar: device is tablet and task is not centered");
             return false;
         }
-        if (task.isTaskSplit() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
-            LauncherInstrumentation.log("Not expecting an actions bar: "
-                    + "device is phone and task is split");
+        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.
             return false;
         }
-        LauncherInstrumentation.log("Expecting an actions bar");
+        testLogD(TAG, "Expecting an actions bar");
         return true;
     }
 
@@ -473,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");
@@ -499,22 +529,27 @@
             throw new IllegalStateException("Must be run on tablet device.");
         }
         final List<UiObject2> taskViews = getTasks();
-        if (!hasTasks()) {
-            LauncherInstrumentation.log("no recent tasks");
+        if (taskViews.isEmpty()) {
             return null;
         }
-        int focusedTaskHeight = mLauncher.getFocusedTaskHeightForTablet();
+        Rect focusTaskSize = mLauncher.getOverviewTaskSize();
+        int focusedTaskHeight = focusTaskSize.height();
         for (UiObject2 task : taskViews) {
             OverviewTask overviewTask = new OverviewTask(mLauncher, task, this);
-
-            LauncherInstrumentation.log("checking task height ("
-                    + overviewTask.getVisibleHeight()
-                    + ") against defined focused task height ("
-                    + focusedTaskHeight + ")");
+            // Desktop tasks can't be focused tasks, but are the same size.
+            if (overviewTask.isDesktop()) {
+                continue;
+            }
             if (overviewTask.getVisibleHeight() == focusedTaskHeight) {
                 return overviewTask;
             }
         }
         return null;
     }
+
+    protected boolean isLiveTile(UiObject2 task) {
+        // UiObject2.equals returns false even when mLiveTileTask and task have the same node, hence
+        // compare only hashCode as a workaround.
+        return mLiveTileTask != null && mLiveTileTask.hashCode() == task.hashCode();
+    }
 }
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/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 200f2ff..b3ad930 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -33,6 +33,7 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.NonNull;
 import androidx.test.uiautomator.Condition;
 import androidx.test.uiautomator.UiDevice;
 
@@ -75,6 +76,20 @@
         return false;
     }
 
+    @NonNull
+    @Override
+    public BaseOverview switchToOverview() {
+        try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
+                     "want to switch from background to overview")) {
+            verifyActiveContainer();
+            goToOverviewUnchecked();
+            return mLauncher.is3PLauncher()
+                    ? new BaseOverview(mLauncher, /*launchedFromApp=*/true)
+                    : new Overview(mLauncher, /*launchedFromApp=*/true);
+        }
+    }
+
     /**
      * Returns the taskbar.
      *
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f02a0c2..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;
@@ -55,6 +54,7 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.Trace;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.InputDevice;
@@ -98,6 +98,7 @@
 import java.util.function.BooleanSupplier;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -427,14 +428,14 @@
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
-    int getFocusedTaskHeightForTablet() {
-        return getTestInfo(TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET).getInt(
-                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    Rect getOverviewTaskSize() {
+        return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_TASK_SIZE)
+                .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD, Rect.class);
     }
 
-    Rect getGridTaskRectForTablet() {
-        return ((Rect) getTestInfo(TestProtocol.REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET)
-                .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD));
+    Rect getOverviewGridTaskSize() {
+        return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_GRID_TASK_SIZE)
+                .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD, Rect.class);
     }
 
     int getOverviewPageSpacing() {
@@ -455,10 +456,6 @@
         getTestInfo(TestProtocol.REQUEST_ENABLE_ROTATION, Boolean.toString(on));
     }
 
-    public void setEnableSuggestion(boolean enableSuggestion) {
-        getTestInfo(TestProtocol.REQUEST_ENABLE_SUGGESTION, Boolean.toString(enableSuggestion));
-    }
-
     public boolean hadNontestEvents() {
         return getTestInfo(TestProtocol.REQUEST_GET_HAD_NONTEST_EVENTS)
                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
@@ -527,16 +524,19 @@
 
     Closable addContextLayer(String piece) {
         mDiagnosticContext.addLast(piece);
+        Trace.beginSection("Context: " + piece);
         log("Entering context: " + piece);
         return () -> {
+            Trace.endSection();
             log("Leaving context: " + piece);
             mDiagnosticContext.removeLast();
         };
     }
 
     public void dumpViewHierarchy() {
-        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
         try {
+            Trace.beginSection("dumpViewHierarchy");
+            final ByteArrayOutputStream stream = new ByteArrayOutputStream();
             mDevice.dumpWindowHierarchy(stream);
             stream.flush();
             stream.close();
@@ -545,6 +545,8 @@
             }
         } catch (IOException e) {
             Log.e(TAG, "error dumping XML to logcat", e);
+        } finally {
+            Trace.endSection();
         }
     }
 
@@ -624,15 +626,20 @@
      */
     public void checkForAnomaly(
             boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) {
-        if (mTestAnomalyChecker != null) mTestAnomalyChecker.run();
+        try {
+            Trace.beginSection("checkForAnomaly");
+            if (mTestAnomalyChecker != null) mTestAnomalyChecker.run();
 
-        final String systemAnomalyMessage =
-                getSystemAnomalyMessage(ignoreNavmodeChangeStates, ignoreOnlySystemUiViews);
-        if (systemAnomalyMessage != null) {
-            if (mOnFailure != null) mOnFailure.run();
-            Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
-                    "http://go/tapl : Tests are broken by a non-Launcher system error: "
-                            + systemAnomalyMessage, false)));
+            final String systemAnomalyMessage =
+                    getSystemAnomalyMessage(ignoreNavmodeChangeStates, ignoreOnlySystemUiViews);
+            if (systemAnomalyMessage != null) {
+                if (mOnFailure != null) mOnFailure.run();
+                Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
+                        "http://go/tapl : Tests are broken by a non-Launcher system error: "
+                                + systemAnomalyMessage, false)));
+            }
+        } finally {
+            Trace.endSection();
         }
     }
 
@@ -1008,16 +1015,20 @@
     }
 
     public void waitForLauncherInitialized() {
-        for (int i = 0; i < 100; ++i) {
-            if (getTestInfo(
-                    TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
-                    getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
-                return;
+        try {
+            Trace.beginSection("waitForLauncherInitialized");
+            for (int i = 0; i < 100; ++i) {
+                if (getTestInfo(TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).getBoolean(
+                        TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
+                    return;
+                }
+                SystemClock.sleep(100);
             }
-            SystemClock.sleep(100);
+            checkForAnomaly();
+            fail("Launcher didn't initialize");
+        } finally {
+            Trace.endSection();
         }
-        checkForAnomaly();
-        fail("Launcher didn't initialize");
     }
 
     public boolean isLauncherActivityStarted() {
@@ -1200,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,
@@ -1232,7 +1238,8 @@
     void pressBackImpl() {
         waitForLauncherInitialized();
         final boolean launcherVisible =
-                isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
+                (isTablet() || isTaskbarNavbarUnificationEnabled()) ? isLauncherContainerVisible()
+                        : isLauncherVisible();
         boolean isThreeFingerTrackpadGesture =
                 mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
         if (getNavigationModel() == NavigationModel.ZERO_BUTTON
@@ -1261,8 +1268,13 @@
     }
 
     boolean isLauncherVisible() {
-        mDevice.waitForIdle();
-        return hasLauncherObject(getAnyObjectSelector());
+        try {
+            Trace.beginSection("isLauncherVisible");
+            mDevice.waitForIdle();
+            return hasLauncherObject(getAnyObjectSelector());
+        } finally {
+            Trace.endSection();
+        }
     }
 
     boolean isLauncherContainerVisible() {
@@ -1549,8 +1561,6 @@
 
     @NonNull
     UiObject2 waitForLauncherObject(String resName) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForLauncherObject");
         return waitForObjectBySelector(getLauncherObjectSelector(resName));
     }
 
@@ -1580,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;
     }
 
-    private UiObject2 waitForObjectBySelector(BySelector selector) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForObjectBySelector");
+    UiObject2 waitForObjectBySelector(BySelector selector) {
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
         return object;
@@ -1632,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();
@@ -1725,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);
     }
@@ -2034,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++;
@@ -2402,6 +2422,16 @@
         disableSensorRotation();
         final Integer initialPid = getPid();
         final LogEventChecker eventChecker = new LogEventChecker(this);
+        eventChecker.setLogExclusionRule(event -> {
+            Matcher matcher = Pattern.compile("KeyEvent.*flags=0x([0-9a-fA-F]+)").matcher(event);
+            if (matcher.find()) {
+                long keyEventFlags = Long.parseLong(matcher.group(1), 16);
+                // ignore KeyEvents with FLAG_CANCELED
+                return (keyEventFlags & KeyEvent.FLAG_CANCELED) != 0;
+            }
+            return false;
+        });
+
         if (eventChecker.start()) mEventChecker = eventChecker;
 
         return () -> {
@@ -2459,7 +2489,8 @@
     }
 
     float getWindowCornerRadius() {
-        // TODO(b/197326121): Check if the touch is overlapping with the corners by offsetting
+        // Return a larger corner radius to ensure gesture calculated from the radius are offset to
+        // prevent overlapping
         final float tmpBuffer = 100f;
         final Resources resources = getResources();
         if (!supportsRoundedCornersOnWindows(resources)) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 055a357..3a49160 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -35,6 +35,8 @@
     // Map from an event sequence name to an ordered list of expected events in that sequence.
     private final ListMap<Pattern> mExpectedEvents = new ListMap<>();
 
+    private LogExclusionRule mLogExclusionRule = null;
+
     LogEventChecker(LauncherInstrumentation launcher) {
         mLauncher = launcher;
     }
@@ -48,6 +50,10 @@
         mExpectedEvents.add(sequence, pattern);
     }
 
+    void setLogExclusionRule(LogExclusionRule logExclusionRule) {
+        mLogExclusionRule = logExclusionRule;
+    }
+
     // Waits for the expected number of events and returns them.
     private ListMap<String> finishSync(long waitForExpectedCountMs) {
         final long startTime = SystemClock.uptimeMillis();
@@ -74,7 +80,9 @@
         final ListMap<String> eventSequences = new ListMap<>();
         for (String rawEvent : rawEvents) {
             final String[] split = rawEvent.split("/");
-            eventSequences.add(split[0], split[1]);
+            if (mLogExclusionRule == null || !mLogExclusionRule.shouldExclude(split[1])) {
+                eventSequences.add(split[0], split[1]);
+            }
         }
         return eventSequences;
     }
@@ -175,4 +183,8 @@
             return list;
         }
     }
+
+    interface LogExclusionRule {
+        boolean shouldExclude(String event);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 50c2136..deb27e4 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -22,9 +22,12 @@
  * Overview pane.
  */
 public class Overview extends BaseOverview {
-
     Overview(LauncherInstrumentation launcher) {
-        super(launcher);
+        this(launcher, /*launchedFromApp=*/false);
+    }
+
+    Overview(LauncherInstrumentation launcher, boolean launchedFromApp) {
+        super(launcher, launchedFromApp);
     }
 
     @Override
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 6f420af..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;
 
@@ -40,16 +41,23 @@
 public final class OverviewTask {
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
     static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync");
+    static final Pattern TASK_START_EVENT_DESKTOP = Pattern.compile("launchDesktopFromRecents");
+    static final Pattern TASK_START_EVENT_LIVE_TILE = Pattern.compile(
+            "composeRecentsLaunchAnimator");
     static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect");
     static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks");
     private final LauncherInstrumentation mLauncher;
+    @NonNull
     private final UiObject2 mTask;
+    private final TaskViewType mType;
     private final BaseOverview mOverview;
 
-    OverviewTask(LauncherInstrumentation launcher, UiObject2 task, BaseOverview overview) {
+    OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview) {
         mLauncher = launcher;
+        mLauncher.assertNotNull("task must not be null", task);
         mTask = task;
         mOverview = overview;
+        mType = getType(task);
         verifyActiveContainer();
     }
 
@@ -62,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();
     }
 
@@ -95,7 +103,7 @@
      * divider between.
      */
     int getVisibleWidth() {
-        if (isTaskSplit()) {
+        if (isGrouped()) {
             return getCombinedSplitTaskWidth();
         }
 
@@ -149,16 +157,19 @@
                 return;
             }
 
-            boolean taskWasFocused = mLauncher.isTablet() && getVisibleHeight() == mLauncher
-                    .getFocusedTaskHeightForTablet();
+            boolean taskWasFocused = mLauncher.isTablet()
+                    && getVisibleHeight() == mLauncher.getOverviewTaskSize().height();
             List<Integer> originalTasksCenterX =
                     getCurrentTasksCenterXList().stream().sorted().toList();
             boolean isClearAllVisibleBeforeDismiss = mOverview.isClearAllVisible();
 
             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());
                 }
@@ -220,7 +231,22 @@
                     return new LaunchedAppState(mLauncher);
                 }
             } else {
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+                final Pattern event;
+                if (mOverview.isLiveTile(mTask)) {
+                    event = TASK_START_EVENT_LIVE_TILE;
+                } else if (mType == TaskViewType.DESKTOP) {
+                    event = TASK_START_EVENT_DESKTOP;
+                } else {
+                    event = TASK_START_EVENT;
+                }
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, event);
+
+                if (mType == TaskViewType.DESKTOP) {
+                    try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer(
+                            "launched desktop")) {
+                        mLauncher.waitForSystemUiObject("desktop_mode_caption");
+                    }
+                }
                 return new LaunchedAppState(mLauncher);
             }
         }
@@ -234,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")) {
@@ -248,10 +274,6 @@
         }
     }
 
-    boolean isTaskSplit() {
-        return findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes) != null;
-    }
-
     private UiObject2 findObjectInTask(String resName) {
         return mTask.findObject(mLauncher.getOverviewObjectSelector(resName));
     }
@@ -260,10 +282,11 @@
      * Returns whether the given String is contained in this Task's contentDescription. Also returns
      * true if both Strings are null.
      *
-     * TODO(b/326565120): remove Nullable support once the bug causing it to be null is fixed.
+     * TODO(b/342627272): remove Nullable support once the bug causing it to be null is fixed.
      */
-    public boolean containsContentDescription(@Nullable String expected) {
-        String actual = mTask.getContentDescription();
+    public boolean containsContentDescription(@Nullable String expected,
+            OverviewTaskContainer overviewTaskContainer) {
+        String actual = findObjectInTask(overviewTaskContainer.snapshotRes).getContentDescription();
         if (actual == null && expected == null) {
             return true;
         }
@@ -274,22 +297,61 @@
     }
 
     /**
-     * Enum used to specify  which task is retrieved when it is a split task.
+     * Returns whether the given String is contained in this Task's contentDescription. Also returns
+     * true if both Strings are null
      */
-    public enum OverviewSplitTask {
+    public boolean containsContentDescription(@Nullable String expected) {
+        return containsContentDescription(expected, DEFAULT);
+    }
+
+    /**
+     * 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")) {
+            return TaskViewType.DESKTOP;
+        } else {
+            return TaskViewType.SINGLE;
+        }
+    }
+
+    boolean isGrouped() {
+        return mType == TaskViewType.GROUPED;
+    }
+
+    public boolean isDesktop() {
+        return mType == TaskViewType.DESKTOP;
+    }
+
+    /**
+     * Enum used to specify which resource name should be used depending on the type of the task.
+     */
+    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;
         }
     }
+
+    enum TaskViewType {
+        SINGLE,
+        GROUPED,
+        DESKTOP
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index 902ad5b..90d32f3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -97,6 +97,29 @@
         }
     }
 
+    /**
+     * Taps the Desktop item from the overview task menu and returns the LaunchedAppState
+     * representing the Desktop.
+     */
+    @NonNull
+    public LaunchedAppState tapDesktopMenuItem() {
+        try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
+                     "before tapping the desktop menu item")) {
+            mLauncher.executeAndWaitForLauncherStop(
+                    () -> mLauncher.clickLauncherObject(
+                            mLauncher.findObjectInContainer(mMenu, By.text("Desktop"))),
+                    "tapped desktop menu item");
+
+            try (LauncherInstrumentation.Closable ignored2 = mLauncher.addContextLayer(
+                    "tapped desktop menu item")) {
+                mLauncher.waitUntilSystemLauncherObjectGone("overview_panel");
+                mLauncher.waitForSystemUiObject("desktop_mode_caption");
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
     /** Returns true if an item matching the given string is present in the menu. */
     public boolean hasMenuItem(String expectedMenuItemText) {
         UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));
@@ -104,14 +127,6 @@
     }
 
     /**
-     * Returns the menu item specified by name if present.
-     */
-    public OverviewTaskMenuItem getMenuItemByName(String menuItemName) {
-        return new OverviewTaskMenuItem(mLauncher,
-                mLauncher.waitForObjectInContainer(mMenu, By.text(menuItemName)));
-    }
-
-    /**
      * Taps outside task menu to dismiss it.
      */
     public void touchOutsideTaskMenuToDismiss() {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
deleted file mode 100644
index e3035bf..0000000
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
+++ /dev/null
@@ -1,39 +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.tapl;
-
-import android.graphics.Rect;
-
-import androidx.test.uiautomator.UiObject2;
-
-/** Represents an item in the overview task menu. */
-public class OverviewTaskMenuItem {
-
-    private final LauncherInstrumentation mLauncher;
-    private final UiObject2 mMenuItem;
-
-    OverviewTaskMenuItem(LauncherInstrumentation launcher, UiObject2 menuItem) {
-        mLauncher = launcher;
-        mMenuItem = menuItem;
-    }
-
-    /**
-     * Returns this menu item's visible bounds.
-     */
-    public Rect getVisibleBounds() {
-        return mMenuItem.getVisibleBounds();
-    }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index e6315f3..b4aaab7 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -52,7 +52,7 @@
 
         if (!mLauncher.isTransientTaskbar()) {
             Assert.assertEquals("Persistent taskbar should fill screen width",
-                    getVisibleBounds().width(), mLauncher.getRealDisplaySize().x);
+                    mLauncher.getRealDisplaySize().x, getVisibleBounds().width());
         }
     }
 
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 9ac6768..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;
 
@@ -345,23 +342,35 @@
      * @return map of text -> center of the view. In case of icons with the same name, the one with
      * lower x coordinate is selected.
      */
-    public Map<String, Point> getWorkspaceIconsPositions() {
+    public Map<String, Point> getAllWorkspaceIconsPositions() {
         final UiObject2 workspace = verifyActiveContainer();
-        mLauncher.waitForLauncherInitialized(); // b/319501259
         List<UiObject2> workspaceIcons =
                 mLauncher.waitForObjectsInContainer(workspace, AppIcon.getAnyAppIconSelector());
-        return workspaceIcons.stream()
+        return getIconPositionMap(workspaceIcons);
+    }
+
+    /**
+     * @return point where icon is found for given the app name,
+     * point is visible center of the icon.
+     */
+    @NonNull
+    public Point getWorkspaceIconPosition(String appName) {
+        final UiObject2 workspace = verifyActiveContainer();
+
+        UiObject2 workspaceIcon =
+                mLauncher.waitForObjectInContainer(workspace,
+                        AppIcon.getAppIconSelector(appName, mLauncher));
+        return workspaceIcon.getVisibleCenter();
+    }
+
+    private Map<String, Point> getIconPositionMap(List<UiObject2> icons) {
+        return icons.stream()
                 .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));
@@ -629,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,