Merge "Make widget sheet layout container not focusable" into main
diff --git a/Android.bp b/Android.bp
index bcbd362..9d7aa73 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.
@@ -64,11 +64,74 @@
 filegroup {
     name: "launcher-quickstep-src",
     srcs: [
-        "quickstep/src/**/*.java",
         "quickstep/src/**/*.kt",
+        "quickstep/src/**/*.java",
+        ":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",
+    ],
+}
+
+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"],
+}
+
+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",
@@ -405,6 +468,7 @@
         "SystemUISharedLib",
         "SettingsLibSettingsTheme",
         "dagger2",
+        "protolog-group",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     min_sdk_version: "current",
@@ -503,7 +567,10 @@
         "Launcher2",
         "Launcher3",
     ],
-    required: ["privapp_whitelist_com.android.launcher3"],
+    required: [
+        "privapp_whitelist_com.android.launcher3",
+        "launcher.quickstep.protolog.pb",
+    ],
 
     resource_dirs: ["quickstep/res"],
 
diff --git a/aconfig/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 0da2df1..9fb5b7b 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"
@@ -388,3 +388,59 @@
     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: "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
+    }
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index 23733a4..853faf8 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"
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/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 4c724dc..2ef9f82 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -64,3 +64,11 @@
         "tests/multivalentScreenshotTests/src/**/*.kt",
     ],
 }
+
+filegroup {
+    name: "launcher3-quickstep-testing",
+    path: "testing",
+    srcs: [
+        "testing/**/*.kt",
+    ],
+}
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index bf198b6..57bfb4a 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -54,6 +54,7 @@
         android:protectionLevel="signature|privileged" />
 
     <application android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+         android:enableOnBackInvokedCallback="true"
          android:fullBackupOnly="true"
          android:fullBackupContent="@xml/backupscheme"
          android:hardwareAccelerated="true"
diff --git a/quickstep/dagger/LauncherAppComponent.java b/quickstep/dagger/LauncherAppComponent.java
index dab2582..068f01c 100644
--- a/quickstep/dagger/LauncherAppComponent.java
+++ b/quickstep/dagger/LauncherAppComponent.java
@@ -16,16 +16,18 @@
 
 package com.android.launcher3.dagger;
 
-import dagger.Component;
 
-import javax.inject.Singleton;
+import com.android.quickstep.dagger.QuickStepModule;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
+
+import dagger.Component;
 
 /**
  * Root component for Dagger injection for Launcher Quickstep.
  */
-@Singleton
-@Component
-public interface LauncherAppComponent extends LauncherBaseAppComponent {
+@LauncherAppSingleton
+@Component(modules = QuickStepModule.class)
+public interface LauncherAppComponent extends QuickstepBaseAppComponent {
     /** Builder for quickstep LauncherAppComponent. */
     @Component.Builder
     interface Builder extends LauncherBaseAppComponent.Builder {
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/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..fc1e914
--- /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_avatar"
+        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_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="@id/bubble_flyout_avatar"
+        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_name"
+        app:layout_constraintStart_toEndOf="@id/bubble_flyout_avatar"
+        tools:text="This is a message"/>
+
+</merge>
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index d90d916..afbcdb5 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -16,6 +16,7 @@
 <com.android.quickstep.task.thumbnail.TaskThumbnailView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/snapshot"
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 
diff --git a/quickstep/res/layout/taskbar_overflow_button.xml b/quickstep/res/layout/taskbar_overflow_button.xml
new file mode 100644
index 0000000..20104f2
--- /dev/null
+++ b/quickstep/res/layout/taskbar_overflow_button.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.
+-->
+
+<!-- Note: The actual size will match the taskbar icon sizes in TaskbarView#onLayout(). -->
+<com.android.launcher3.views.IconButtonView xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/BaseIcon.Workspace.Taskbar"
+    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/values-af/strings.xml b/quickstep/res/values-af/strings.xml
index 4122637..7d65403 100644
--- a/quickstep/res/values-af/strings.xml
+++ b/quickstep/res/values-af/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Skuif na links bo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Skuif na regs onder"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{meer app}other{meer apps}}"</string>
diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml
index 956767ee..0848ddd 100644
--- a/quickstep/res/values-am/strings.xml
+++ b/quickstep/res/values-am/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ሁልጊዜ የተግባር አሞሌ ያሳዩ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"የአሰሳ ሁነታን ይለውጡ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"የተግባር አሞሌ አካፋይ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"የተግባር አሞሌ ትርፍ ፍሰት"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ወደ ላይ/ግራ ይውሰዱ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ወደ ታች/ቀኝ ይውሰዱ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ተጨማሪ መተግበሪያ}one{ተጨማሪ መተግበሪያ}other{ተጨማሪ መተግበሪያዎች}}"</string>
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index 29d214d..6ba2e5d 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -89,7 +89,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>
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"الانتقال إلى يمين الشاشة أو أعلاها"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"الانتقال إلى يسار الشاشة أو أسفلها"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{تطبيق واحد آخر}zero{تطبيق آخر}two{تطبيقان آخران}few{تطبيقات أخرى}many{تطبيقًا آخر}other{تطبيق آخر}}"</string>
diff --git a/quickstep/res/values-as/strings.xml b/quickstep/res/values-as/strings.xml
index 912003b..486dc09 100644
--- a/quickstep/res/values-as/strings.xml
+++ b/quickstep/res/values-as/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ওপৰৰ বাঁওফাললৈ নিয়ক"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"তলৰ সোঁফাললৈ নিয়ক"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{অধিক এপ্‌}one{অধিক এপ্‌}other{অধিক এপ্‌}}"</string>
diff --git a/quickstep/res/values-az/strings.xml b/quickstep/res/values-az/strings.xml
index b89b707..c483db3 100644
--- a/quickstep/res/values-az/strings.xml
+++ b/quickstep/res/values-az/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Yuxarı/sola köçürün"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Aşağı/sağa köçürün"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{əlavə tətbiq}other{əlavə tətbiq}}"</string>
diff --git a/quickstep/res/values-b+sr+Latn/strings.xml b/quickstep/res/values-b+sr+Latn/strings.xml
index 4ef487e..aa16f3c 100644
--- a/quickstep/res/values-b+sr+Latn/strings.xml
+++ b/quickstep/res/values-b+sr+Latn/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvek prikazuj traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promeni režim navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdelnik trake zadataka"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Preklopna traka zadataka"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premesti gore levo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premesti dole desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
diff --git a/quickstep/res/values-be/strings.xml b/quickstep/res/values-be/strings.xml
index c506acb..4dcfe62 100644
--- a/quickstep/res/values-be/strings.xml
+++ b/quickstep/res/values-be/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Заўсёды паказваць панэль задач"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Змяніць рэжым навігацыі"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Раздзяляльнік панэлі задач"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Меню з пашырэннем панэлі задач"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Перамясціць уверх/улева"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перамясціць уніз/управа"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{даступная праграма}one{даступная праграма}few{даступныя праграмы}many{даступных праграм}other{даступнай праграмы}}"</string>
diff --git a/quickstep/res/values-bg/strings.xml b/quickstep/res/values-bg/strings.xml
index d03e4f7..3c0d840 100644
--- a/quickstep/res/values-bg/strings.xml
+++ b/quickstep/res/values-bg/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Преместване горе/вляво"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Преместване долу/вдясно"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{допълнително приложение}other{допълнителни приложения}}"</string>
diff --git a/quickstep/res/values-bn/strings.xml b/quickstep/res/values-bn/strings.xml
index 0c568d9..a224dac 100644
--- a/quickstep/res/values-bn/strings.xml
+++ b/quickstep/res/values-bn/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"উপরে/বাঁদিকে সরান"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"নিচে/ডানদিকে সরান"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{আরও অ্যাপ}one{আরও অ্যাপ}other{আরও অ্যাপ}}"</string>
diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml
index 922883e..3357a6e 100644
--- a/quickstep/res/values-bs/strings.xml
+++ b/quickstep/res/values-bs/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvijek prikaži traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promijeni način navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdjelnik trake zadataka"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Dodatni izbornik trake sa zadacima"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premjesti gore lijevo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml
index fe7933b..abb2984 100644
--- a/quickstep/res/values-ca/strings.xml
+++ b/quickstep/res/values-ca/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mou a la part superior o a l\'esquerra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mou a la part inferior o a la dreta"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicació més}other{aplicacions més}}"</string>
diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml
index 3047d05..324b02a 100644
--- a/quickstep/res/values-cs/strings.xml
+++ b/quickstep/res/values-cs/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Přesunout doleva nahoru"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Přesunout doprava dolů"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{další aplikace}few{další aplikace}many{další aplikace}other{dalších aplikací}}"</string>
diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml
index 6e2130c..ca30dda 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flyt til toppen eller venstre side"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flyt til bunden eller højre side"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{yderligere app}one{yderligere app}other{yderligere apps}}"</string>
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index 54961fe..f14eddf 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Nach oben / Nach links verschieben"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Nach unten / Nach rechts verschieben"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{weitere App}other{weitere Apps}}"</string>
diff --git a/quickstep/res/values-el/strings.xml b/quickstep/res/values-el/strings.xml
index 6cbb833..e767c74 100644
--- a/quickstep/res/values-el/strings.xml
+++ b/quickstep/res/values-el/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Μετακίνηση επάνω/αριστερά"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Μετακίνηση κάτω/δεξιά"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ακόμη εφαρμογή}other{ακόμη εφαρμογές}}"</string>
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index dcbaa7a..a74a17b 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
diff --git a/quickstep/res/values-en-rCA/strings.xml b/quickstep/res/values-en-rCA/strings.xml
index c00e6cd..e0787ca 100644
--- a/quickstep/res/values-en-rCA/strings.xml
+++ b/quickstep/res/values-en-rCA/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar Divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar Overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index dcbaa7a..a74a17b 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index dcbaa7a..a74a17b 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml
index 2abef91..8fcbe2b 100644
--- a/quickstep/res/values-en-rXC/strings.xml
+++ b/quickstep/res/values-en-rXC/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‎‏‎‏‎‏‎‎‎‎‏‎‎‏‏‎‏‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‎Always show Taskbar‎‏‎‎‏‎"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‎‎‎‏‏‏‎‏‎‏‎‏‎‏‏‎‏‎‎‏‎‎‏‏‎‏‎‎‏‏‏‎‎‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‎‎‎Change navigation mode‎‏‎‎‏‎"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‏‎‏‏‎‏‏‎‎‏‎‎‎‏‏‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‏‏‎‎‎‎‏‏‏‏‎‏‏‏‏‎‏‎‎‎‎‎‎Taskbar Divider‎‏‎‎‏‎"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎Taskbar Overflow‎‏‎‎‏‎"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‎‏‎‏‏‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‎‏‏‎‏‎‏‏‎‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‎Move to top/left‎‏‎‎‏‎"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‏‏‏‎‎‏‏‎‏‏‏‏‎‎‎‎‏‎‎Move to bottom/right‎‏‎‎‏‎"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‎‎‎‎‎more app‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‎‎‎‎‎more apps‎‏‎‎‏‎}}"</string>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index f09fae6..e6efd43 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover a la parte superior o izquierda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover a la parte inferior o derecha"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app más}other{apps más}}"</string>
diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml
index 356a38b..d8bbc55 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barra de tareas visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambiar el modo de navegación"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor de Barra de Tareas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tareas ampliada"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover arriba/a la izquierda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover abajo/a la derecha"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicación más}other{aplicaciones más}}"</string>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index fccbeda..0e222bc 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Teisalda üles/vasakule"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Teisalda alla/paremale"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{rakendus veel}other{rakendust veel}}"</string>
diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml
index a1d8cc3..7764cb8 100644
--- a/quickstep/res/values-eu/strings.xml
+++ b/quickstep/res/values-eu/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Eraman gora, ezkerretara"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Eraman behera, eskuinetara"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikazio gehiago}other{aplikazio gehiago}}"</string>
diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml
index 8f37686..05cf83e 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"انتقال به بالا/ چپ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"انتقال به پایین/ راست"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{برنامه دیگر}one{برنامه دیگر}other{برنامه دیگر}}"</string>
diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml
index 175a896..73b1ba4 100644
--- a/quickstep/res/values-fi/strings.xml
+++ b/quickstep/res/values-fi/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Siirrä ylös tai vasemmalle"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Siirrä alas tai oikealle"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{muu sovellus}other{muuta sovellusta}}"</string>
diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index 6d6c67c..a554881 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Déplacer vers le coin supérieur gauche de l\'écran"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer vers le coin inférieur droit de l\'écran"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{autre appli}one{autre appli}other{autres applis}}"</string>
diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml
index a395266..3711dcd 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Déplacer en haut ou à gauche"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer en bas ou à droite"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{autre application}one{autre application}other{autres applications}}"</string>
diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml
index 5b04960..a2c4b73 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover á parte superior ou á esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover á parte inferior ou á dereita"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicación máis}other{aplicacións máis}}"</string>
diff --git a/quickstep/res/values-gu/strings.xml b/quickstep/res/values-gu/strings.xml
index fc253bd..dea52c3 100644
--- a/quickstep/res/values-gu/strings.xml
+++ b/quickstep/res/values-gu/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"સૌથી ઉપર ડાબી બાજુએ ખસેડો"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"સૌથી નીચે જમણી બાજુએ ખસેડો"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{વધુ ઍપ}one{વધુ ઍપ}other{વધુ ઍપ}}"</string>
diff --git a/quickstep/res/values-hi/strings.xml b/quickstep/res/values-hi/strings.xml
index 30a17db..e81e942 100644
--- a/quickstep/res/values-hi/strings.xml
+++ b/quickstep/res/values-hi/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ऊपर/बाईं तरफ़ ले जाएं"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"नीचे/दाईं तरफ़ ले जाएं"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ज़्यादा ऐप्लिकेशन}one{ज़्यादा ऐप्लिकेशन}other{ज़्यादा ऐप्लिकेशन}}"</string>
diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml
index 06511e9..2514aa1 100644
--- a/quickstep/res/values-hr/strings.xml
+++ b/quickstep/res/values-hr/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvijek prikaži traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promijeni način navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdjelnik trake sa zadacima"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Dodatni izbornik trake sa zadacima"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premjesti gore/lijevo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje/desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
diff --git a/quickstep/res/values-hu/strings.xml b/quickstep/res/values-hu/strings.xml
index 9bd9478..4dc9974 100644
--- a/quickstep/res/values-hu/strings.xml
+++ b/quickstep/res/values-hu/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mozgatás felülre vagy a bal oldalra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mozgatás alulra vagy a jobb oldalra"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{további alkalmazás}other{további alkalmazás}}"</string>
diff --git a/quickstep/res/values-hy/strings.xml b/quickstep/res/values-hy/strings.xml
index e1481ec..f4fcacf 100644
--- a/quickstep/res/values-hy/strings.xml
+++ b/quickstep/res/values-hy/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Տեղափոխել վերևի ձախ անկյուն"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Տեղափոխել ներքևի աջ անկյուն"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{լրացուցիչ հավելված}one{լրացուցիչ հավելված}other{լրացուցիչ հավելված}}"</string>
diff --git a/quickstep/res/values-in/strings.xml b/quickstep/res/values-in/strings.xml
index f8345b5..519dced 100644
--- a/quickstep/res/values-in/strings.xml
+++ b/quickstep/res/values-in/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Pindahkan ke atas/kiri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pindahkan ke bawah/kanan"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikasi lainnya}other{aplikasi lainnya}}"</string>
diff --git a/quickstep/res/values-is/strings.xml b/quickstep/res/values-is/strings.xml
index 017d108..3aec0ce 100644
--- a/quickstep/res/values-is/strings.xml
+++ b/quickstep/res/values-is/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Alltaf sýna forritastiku"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Breyta leiðsagnarstillingu"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Skipting forritastiku"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Yfirflæði á forritastiku"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Færa efst/til vinstri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Færa neðst/til hægri"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{forrit til viðbótar}one{forrit til viðbótar}other{forrit til viðbótar}}"</string>
diff --git a/quickstep/res/values-it/strings.xml b/quickstep/res/values-it/strings.xml
index 1983ee3..445474e 100644
--- a/quickstep/res/values-it/strings.xml
+++ b/quickstep/res/values-it/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sposta in alto/a sinistra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sposta in basso/a destra"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{altra app}other{altre app}}"</string>
diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml
index a7115e3..5bb51ce 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"העברה לפינה השמאלית/העליונה"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"העברה לפינה הימנית/התחתונה"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{אפליקציה נוספת}one{אפליקציות נוספות}two{אפליקציות נוספות}other{אפליקציות נוספות}}"</string>
diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml
index fa3c01d..28b7746 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"常にタスクバーを表示する"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ナビゲーション モードを変更"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"タスクバーの区切り"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"タスクバーのオーバフロー"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"上 / 左に移動"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"下 / 右に移動"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個のその他のアプリ}other{個のその他のアプリ}}"</string>
diff --git a/quickstep/res/values-ka/strings.xml b/quickstep/res/values-ka/strings.xml
index a032731..d84d53e 100644
--- a/quickstep/res/values-ka/strings.xml
+++ b/quickstep/res/values-ka/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ამოცანათა ზოლის მუდამ ჩვენება"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"შეცვალეთ ნავიგაციის რეჟიმი"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ამოცანათა ზოლის გამყოფი"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ამოცანათა ზოლის გადავსება"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ზემოთ/მარცხნივ გადატანა"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ქვემოთ/მარჯვნივ გადატანა"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{სხვა აპი}other{სხვა აპი}}"</string>
diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml
index e1a9e8e..4cdbfc4 100644
--- a/quickstep/res/values-kk/strings.xml
+++ b/quickstep/res/values-kk/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Тапсырма жолағын үнемі көрсету"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Навигация режимін өзгерту"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Тапсырмалар жолағын бөлгіш"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"\"Тапсырмалар жолағы\" қосымша мәзірі"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Жоғары/солға жылжыту"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмен/оңға жылжыту"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{қосымша қолданба}other{қосымша қолданба}}"</string>
diff --git a/quickstep/res/values-km/strings.xml b/quickstep/res/values-km/strings.xml
index 2eb3114..5cf1b92 100644
--- a/quickstep/res/values-km/strings.xml
+++ b/quickstep/res/values-km/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"បង្ហាញរបារកិច្ចការជានិច្ច"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ប្ដូរ​មុខងាររុករក"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"បន្ទាត់​ខណ្ឌចែករបារកិច្ចការ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ម៉ឺនុយបន្ថែមរបារកិច្ចការ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ផ្លាស់ទីទៅខាងលើ/ឆ្វេង"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ផ្លាស់ទីទៅខាងក្រោម/ស្ដាំ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{កម្មវិធីច្រើនទៀត}other{កម្មវិធីច្រើនទៀត}}"</string>
diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml
index bb60620..9a04ffd 100644
--- a/quickstep/res/values-kn/strings.xml
+++ b/quickstep/res/values-kn/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ಮೇಲಿನ/ಎಡಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ಕೆಳಗಿನ/ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌}one{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌ಗಳು}other{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌ಗಳು}}"</string>
diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml
index e6a80c3..38d5bbb 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"상단/왼쪽으로 이동"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"하단/오른쪽으로 이동"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{추가 앱}other{추가 앱}}"</string>
diff --git a/quickstep/res/values-ky/strings.xml b/quickstep/res/values-ky/strings.xml
index 98f46ac..faf5675 100644
--- a/quickstep/res/values-ky/strings.xml
+++ b/quickstep/res/values-ky/strings.xml
@@ -92,7 +92,7 @@
     <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="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>
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Такта ар дайым көрүнсүн"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Өтүү режимин өзгөртүү"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Тапшырмалар панелин бөлгүч"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"\"Тапшырмалар панели\" кошумча менюсу"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Жогорку/сол бурчка жылдыруу"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмөнкү/оң бурчка жылдыруу"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{колдонмо бар}other{колдонмо бар}}"</string>
diff --git a/quickstep/res/values-land/dimens.xml b/quickstep/res/values-land/dimens.xml
index 2239f8b..efdc7de 100644
--- a/quickstep/res/values-land/dimens.xml
+++ b/quickstep/res/values-land/dimens.xml
@@ -81,7 +81,7 @@
     <dimen name="taskbar_contextual_button_suw_margin">48dp</dimen>
     <dimen name="taskbar_contextual_button_suw_height">48dp</dimen>
     <dimen name="taskbar_suw_frame">96dp</dimen>
-    <dimen name="taskbar_suw_insets">24dp</dimen>
+    <dimen name="taskbar_suw_insets">48dp</dimen>
 
     <!-- Keyboard Quick Switch -->
     <dimen name="keyboard_quick_switch_taskview_width">217.6dp</dimen>
diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml
index b3ca116..284cdc7 100644
--- a/quickstep/res/values-lo/strings.xml
+++ b/quickstep/res/values-lo/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ຍ້າຍໄປຊ້າຍ/ເທິງ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ຍ້າຍໄປຂວາ/ລຸ່ມ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ແອັບເພີ່ມເຕີມ}other{ແອັບເພີ່ມເຕີມ}}"</string>
diff --git a/quickstep/res/values-lt/strings.xml b/quickstep/res/values-lt/strings.xml
index 4f3f36e..a95249b 100644
--- a/quickstep/res/values-lt/strings.xml
+++ b/quickstep/res/values-lt/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Visada rodyti užduočių juostą"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Keisti naršymo režimą"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Užduočių juostos daliklis"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Užduočių juostos perpildymas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Perkelti aukštyn, kairėn"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Perkelti žemyn, dešinėn"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{papildoma programa}one{papildoma programa}few{papildomos programos}many{papildomos programos}other{papildomų programų}}"</string>
diff --git a/quickstep/res/values-lv/strings.xml b/quickstep/res/values-lv/strings.xml
index fd75fc4..408f3c3 100644
--- a/quickstep/res/values-lv/strings.xml
+++ b/quickstep/res/values-lv/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Pārvietot uz augšējo/kreiso stūri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pārvietot uz apakšējo/labo stūri"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{papildu lietotne}zero{papildu lietotņu}one{papildu lietotne}other{papildu lietotnes}}"</string>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index b50277b..588578e 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Премести горе лево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести долу десно"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{дополнителна апликација}one{дополнителна апликација}other{дополнителни апликации}}"</string>
diff --git a/quickstep/res/values-ml/strings.xml b/quickstep/res/values-ml/strings.xml
index ef72da1..c15a241 100644
--- a/quickstep/res/values-ml/strings.xml
+++ b/quickstep/res/values-ml/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ടാസ്‌ക്ബാർ എപ്പോഴും കാണിക്കൂ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"നാവിഗേഷൻ മോഡ് മാറ്റുക"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ടാസ്‌ക്ബാർ ഡിവൈഡർ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ടാസ്‌ക്ബാർ ഓവർഫ്ലോ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"മുകളിലേക്കോ ഇടത്തേക്കോ നീക്കുക"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"താഴേക്കോ വലത്തേക്കോ നീക്കുക"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{കൂടുതൽ ആപ്പ്}other{കൂടുതൽ ആപ്പുകൾ}}"</string>
diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml
index 2ca2e26..9223139 100644
--- a/quickstep/res/values-mn/strings.xml
+++ b/quickstep/res/values-mn/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Зүүн дээд хэсэг рүү зөөх"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Баруун доод хэсэг рүү зөөх"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{бусад апп}other{бусад апп}}"</string>
diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml
index 159368d..3eef060 100644
--- a/quickstep/res/values-mr/strings.xml
+++ b/quickstep/res/values-mr/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"सर्वात वरती/डावीकडे हलवा"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"तळाशी/उजवीकडे हलवा"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{आणखी अ‍ॅप}other{आणखी अ‍ॅप्स}}"</string>
diff --git a/quickstep/res/values-ms/strings.xml b/quickstep/res/values-ms/strings.xml
index 9a27e51..a1f19a9 100644
--- a/quickstep/res/values-ms/strings.xml
+++ b/quickstep/res/values-ms/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Papar Bar Tugas selalu"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Tukar mod navigasi"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Pembahagi Bar Tugas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Limpahan Bar Tugas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Alihkan ke atas/kiri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Alihkan ke bawah/kanan"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{apl lagi}other{apl lagi}}"</string>
diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml
index 7e298fb..6ea946a 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"အပေါ်/ဘယ်ဘက်သို့ ရွှေ့ရန်"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"အောက်ခြေ/ညာဘက်သို့ ရွှေ့ရန်"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{နောက်ထပ်အက်ပ်}other{နောက်ထပ်အက်ပ်များ}}"</string>
diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml
index 4b982ef..33cde5a 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -97,7 +97,7 @@
     <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="1939025102486630426">"Avbryt"</string>
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flytt til øverst/venstre"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytt til nederst/høyre"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app til}other{apper til}}"</string>
diff --git a/quickstep/res/values-ne/strings.xml b/quickstep/res/values-ne/strings.xml
index 326222b..a2d4d32 100644
--- a/quickstep/res/values-ne/strings.xml
+++ b/quickstep/res/values-ne/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"टास्कबार सधैँ देखाउनुहोस्"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"नेभिगेसन मोड बदल्नुहोस्"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"टास्कबार डिभाइडर"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"टास्कबार ओभरफ्लो"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"सिरान/बायाँतिर सार्नुहोस्"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"फेद/दायाँतिर सार्नुहोस्"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{थप एप}other{थप एपहरू}}"</string>
diff --git a/quickstep/res/values-night/colors.xml b/quickstep/res/values-night/colors.xml
index 2052446..98e4871 100644
--- a/quickstep/res/values-night/colors.xml
+++ b/quickstep/res/values-night/colors.xml
@@ -25,7 +25,7 @@
     <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_turn_on_stroke">?attr/materialColorPrimary</color>
     <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
     <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml
index 8a923b5..a4949ac 100644
--- a/quickstep/res/values-nl/strings.xml
+++ b/quickstep/res/values-nl/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Naar boven/links verplaatsen"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Naar beneden/rechts verplaatsen"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{extra app}other{extra apps}}"</string>
diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml
index 3150ded..7753153 100644
--- a/quickstep/res/values-or/strings.xml
+++ b/quickstep/res/values-or/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ଶୀର୍ଷ/ବାମକୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ନିମ୍ନ/ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ଅଧିକ ଆପ}other{ଅଧିକ ଆପ୍ସ}}"</string>
diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml
index 7da7555..011fa6e 100644
--- a/quickstep/res/values-pa/strings.xml
+++ b/quickstep/res/values-pa/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ਸਿਖਰਲੇ/ਖੱਬੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ਹੇਠਾਂ/ਸੱਜੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ਹੋਰ ਐਪ}one{ਹੋਰ ਐਪ}other{ਹੋਰ ਐਪਾਂ}}"</string>
diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml
index 0cc49e2..df0f74c 100644
--- a/quickstep/res/values-pl/strings.xml
+++ b/quickstep/res/values-pl/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Przesuń w górny lewy róg"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Przesuń w dolny prawy róg"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{inna aplikacja}few{inne aplikacje}many{innych aplikacji}other{innej aplikacji}}"</string>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index 33b87df..2167875 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ver sempre Barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Alterar modo de navegação"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Menu adicional da Barra de tarefas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover para a parte superior esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para a part superior direita"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{outra app}other{outras apps}}"</string>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index 0aa6295..9309810 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Sempre mostrar a Barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Mudar o modo de navegação"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Separador da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tarefas flutuante"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover para cima/para a esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para baixo/para a direita"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{outro app}one{outro app}other{outros apps}}"</string>
diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml
index 2f9d287..6bd3e0f 100644
--- a/quickstep/res/values-ro/strings.xml
+++ b/quickstep/res/values-ro/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mută în stânga sus"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mută în dreapta jos"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicație suplimentară}few{mai multe aplicații}other{mai multe aplicații}}"</string>
diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml
index 4054eb7..76c4e1f 100644
--- a/quickstep/res/values-ru/strings.xml
+++ b/quickstep/res/values-ru/strings.xml
@@ -93,7 +93,7 @@
     <string name="allset_button_hint" msgid="2395219947744706291">"Нажмите кнопку главного экрана, чтобы открыть его."</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Теперь вы можете использовать <xliff:g id="DEVICE">%1$s</xliff:g>."</string>
     <string name="default_device_name" msgid="6660656727127422487">"устройство"</string>
-    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Системные настройки навигации"</annotation></string>
+    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Настройки навигации в системе"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Поделиться"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
     <string name="action_split" msgid="2098009717623550676">"Разделить"</string>
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Всегда показывать панель задач"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Изменить режим навигации"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделитель панели задач"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Дополнительное меню панели задач"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Переместить вверх или влево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Переместить вниз или вправо"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{дополнительное приложение}one{дополнительное приложение}few{дополнительных приложения}many{дополнительных приложений}other{дополнительного приложения}}"</string>
diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml
index 19b61f7..0953b38 100644
--- a/quickstep/res/values-si/strings.xml
+++ b/quickstep/res/values-si/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"සෑම විටම කාර්ය තීරුව පෙන්වන්න"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"සංචාලන ප්‍රකාරය වෙනස් කරන්න"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"කාර්ය තීරු බෙදනය"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"කාර්ය තීරුව පිටාර යාම"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ඉහළ/වම වෙත ගෙන යන්න"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"පහළ/දකුණ වෙත ගෙන යන්න"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{තව යෙදුම}one{තවත් යෙදුම්}other{තවත් යෙදුම්}}"</string>
diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml
index 0f0ec27..6241ad2 100644
--- a/quickstep/res/values-sk/strings.xml
+++ b/quickstep/res/values-sk/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Presunúť hore alebo doľava"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Presunúť dole alebo doprava"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ďalšia aplikácia}few{ďalšie aplikácie}many{ďalšie aplikácie}other{ďalšie aplikácie}}"</string>
diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml
index 972ced5..94de1e05 100644
--- a/quickstep/res/values-sl/strings.xml
+++ b/quickstep/res/values-sl/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Stalen prikaz oprav. vrstice"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Spreminjanje načina navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdelilnik opravilne vrstice"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Oblaček opravilne vrstice z dodatnimi elementi"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premakni na vrh/levo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premakni na dno/desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}two{dodatni aplikaciji}few{dodatne aplikacije}other{dodatnih aplikacij}}"</string>
diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml
index d2a7281..12635c6 100644
--- a/quickstep/res/values-sq/strings.xml
+++ b/quickstep/res/values-sq/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Lëviz në krye/majtas"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Lëviz në fund/djathtas"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikacion tjetër}other{aplikacione të tjera}}"</string>
diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml
index 81204af..d6e5d03 100644
--- a/quickstep/res/values-sr/strings.xml
+++ b/quickstep/res/values-sr/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Увек приказуј траку задатака"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Промени режим навигације"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделник траке задатака"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Преклопна трака задатака"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Премести горе лево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести доле десно"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{додатна апликација}one{додатна апликација}few{додатне апликације}other{додатних апликација}}"</string>
diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml
index 5d0c7a3..bba98c6 100644
--- a/quickstep/res/values-sv/strings.xml
+++ b/quickstep/res/values-sv/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Visa alltid aktivitetsfältet"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Ändra navigeringsläge"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Avdelare för aktivitetsfältet"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Fler alternativ för aktivitetsfältet"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flytta högst upp/till vänster"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytta längst ned/till höger"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app till}other{appar till}}"</string>
diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml
index e133ba3..5b74600 100644
--- a/quickstep/res/values-sw/strings.xml
+++ b/quickstep/res/values-sw/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sogeza juu/kushoto"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sogeza chini/kulia"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{programu nyingine}other{programu zingine}}"</string>
diff --git a/quickstep/res/values-sw600dp-land/dimens.xml b/quickstep/res/values-sw600dp-land/dimens.xml
index 5e9a177..49239aa 100644
--- a/quickstep/res/values-sw600dp-land/dimens.xml
+++ b/quickstep/res/values-sw600dp-land/dimens.xml
@@ -33,4 +33,6 @@
     <!-- The bottom margin above the bottom row of tasks in grid only overview -->
     <dimen name="overview_bottom_margin_grid_only">40dp</dimen>
 
+    <dimen name="taskbar_suw_insets">24dp</dimen>
+
 </resources>
diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml
index 95d0fa9..39dc6bd 100644
--- a/quickstep/res/values-ta/strings.xml
+++ b/quickstep/res/values-ta/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"மேலே/இடதுபுறம் நகர்த்தும்"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"கீழே/வலதுபுறம் நகர்த்தும்"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{கூடுதல் ஆப்ஸ்}other{கூடுதல் ஆப்ஸ்}}"</string>
diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml
index 116d388..91ef846 100644
--- a/quickstep/res/values-te/strings.xml
+++ b/quickstep/res/values-te/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"టాస్క్‌బార్‌ను నిరంతరం చూపండి"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"నావిగేషన్ మోడ్‌ను మార్చండి"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"టాస్క్‌బార్ డివైడర్"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"టాస్క్‌బార్ ఓవర్‌ఫ్లో"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ఎగువ/ఎడమ వైపునకు తరలించండి"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"దిగువ/కుడి వైపునకు తరలించండి"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{మరో యాప్‌}other{మరిన్ని యాప్‌లు}}"</string>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index ecdb3b5..c6fddfb 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ย้ายไปที่ด้านบนหรือด้านซ้าย"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ย้ายไปที่ด้านล่างหรือด้านขวา"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{แอปเพิ่มเติม}other{แอปเพิ่มเติม}}"</string>
diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml
index da646a4..91f9675 100644
--- a/quickstep/res/values-tl/strings.xml
+++ b/quickstep/res/values-tl/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Ilipat sa itaas/kaliwa"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Ilipat sa ibaba/kanan"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{pang app}one{pang app}other{pang app}}"</string>
diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml
index 7be5464..f5f98bb 100644
--- a/quickstep/res/values-tr/strings.xml
+++ b/quickstep/res/values-tr/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sol üste taşı"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sağ alta taşı"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{uygulama daha}other{uygulama daha}}"</string>
diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml
index 216ae0b..93d974f 100644
--- a/quickstep/res/values-uk/strings.xml
+++ b/quickstep/res/values-uk/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Перемістити вгору або вліво"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перемістити вниз або вправо"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{інший додаток}one{інший додаток}few{інші додатки}many{інших додатків}other{іншого додатка}}"</string>
diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml
index 2307cb1..76a2c81 100644
--- a/quickstep/res/values-ur/strings.xml
+++ b/quickstep/res/values-ur/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"اوپر/بائیں طرف منتقل کریں"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"نیچے/دائیں طرف منتقل کریں"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{مزید ایپ}other{مزید ایپس}}"</string>
diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml
index 9f98b55..0d6c09d 100644
--- a/quickstep/res/values-uz/strings.xml
+++ b/quickstep/res/values-uz/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Yuqoriga yoki chapga oʻtkazish"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pastga yoki oʻngga oʻtkazish"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{boshqa ilova}other{boshqa ilovalar}}"</string>
diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml
index 50acbbc..cf38392 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Chuyển lên trên cùng/sang bên trái"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Chuyển xuống dưới cùng/sang bên phải"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ứng dụng khác}other{ứng dụng khác}}"</string>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index 1b6c4e4..9017482 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移到顶部/左侧"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右侧"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{多个应用}other{多个应用}}"</string>
diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml
index 38608e5..672a61c 100644
--- a/quickstep/res/values-zh-rHK/strings.xml
+++ b/quickstep/res/values-zh-rHK/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移至上方/左側"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移至底部/右側"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個其他應用程式}other{個其他應用程式}}"</string>
diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml
index 3e46779..257c996 100644
--- a/quickstep/res/values-zh-rTW/strings.xml
+++ b/quickstep/res/values-zh-rTW/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移到上方/左側"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右側"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個其他應用程式}other{個其他應用程式}}"</string>
diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml
index 120b387..9d7ecd7 100644
--- a/quickstep/res/values-zu/strings.xml
+++ b/quickstep/res/values-zu/strings.xml
@@ -139,6 +139,8 @@
     <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>
+    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
+    <skip />
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Hamba phezulu/kwesokunxele"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Hamba phansi/kwesokudla"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{i-app eyengeziwe}one{ama-app engeziwe}other{ama-app engeziwe}}"</string>
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 0bb971e..4c48bd3 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -94,7 +94,7 @@
     <color name="lottie_yellow600">#f9ab00</color>
 
     <!-- Turn on work apps button -->
-    <color name="work_turn_on_stroke">?androidprv:attr/colorAccentPrimaryVariant</color>
+    <color name="work_turn_on_stroke">?attr/materialColorPrimary</color>
     <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
     <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index ce3f3ac..8957e0d 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -350,7 +350,6 @@
     <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>
@@ -480,6 +479,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 -->
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index f72f3c5..008766b 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -320,6 +320,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). -->
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index e940553..a63ba0f 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -26,7 +26,6 @@
 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;
@@ -210,7 +209,7 @@
          * animation finished runnable.
          */
         @Override
-        public void onAnimationFinished() throws RemoteException {
+        public void onAnimationFinished() {
             mASyncFinishRunnable.run();
         }
     }
@@ -240,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/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 621d226..18337d3 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -46,15 +46,11 @@
 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.testing.shared.TestProtocol.WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE;
 import static com.android.launcher3.util.DisplayController.isTransientTaskbar;
@@ -88,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;
@@ -115,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,14 +136,15 @@
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.StableViewInfo;
-import com.android.launcher3.util.Themes;
 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;
@@ -182,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);
 
@@ -258,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;
@@ -327,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();
 
@@ -354,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);
     }
 
@@ -374,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
@@ -398,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 = StableViewInfo.toLaunchCookie(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
@@ -645,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();
             };
         }
@@ -1203,24 +1137,13 @@
      * 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 */));
-        }
     }
 
     /**
@@ -1232,7 +1155,7 @@
             return;
         }
 
-        mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */);
+        mWallpaperOpenTransitionRunner = new WallpaperOpenLauncherAnimationRunner();
         mLauncherOpenTransition = new RemoteTransition(
                 new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner,
                         false /* startAtFrontOfQueue */).toRemoteTransition(),
@@ -1287,7 +1210,6 @@
         // definition so we don't have to wait for the system gc
         mWallpaperOpenRunner = null;
         mAppLaunchRunner = null;
-        mKeyguardGoingAwayRunner = null;
     }
 
     protected void unregisterRemoteTransitions() {
@@ -1345,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) {
@@ -1494,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());
         }
@@ -1690,20 +1582,48 @@
      * 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()
@@ -1711,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();
@@ -1806,7 +1723,7 @@
             }
         }
 
-        return new Pair(rectFSpringAnim, anim);
+        return new AnimatorBackState(rectFSpringAnim, anim);
     }
 
     public static int getTaskbarToHomeDuration() {
@@ -1826,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,
@@ -1862,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);
         }
     }
 
@@ -1937,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 "
@@ -1969,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;
             }
@@ -1994,8 +1895,7 @@
 
             return new ContainerAnimationRunner(
                     new ActivityTransitionAnimator.AnimationDelegate(
-                            MAIN_EXECUTOR, controller, callback, listener),
-                    cookie);
+                            MAIN_EXECUTOR, controller, callback, listener));
         }
 
         /**
@@ -2005,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;
@@ -2036,8 +1936,8 @@
 
                 @Nullable
                 @Override
-                public ActivityTransitionAnimator.TransitionCookie getTransitionCookie() {
-                    return cookie;
+                public WindowAnimationState getWindowAnimatorState() {
+                    return windowState;
                 }
             };
         }
@@ -2051,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);
         }
@@ -2134,11 +1979,6 @@
         public void onAnimationCancelled() {
             mDelegate.onAnimationCancelled();
         }
-
-        @Override
-        public boolean supportsReturnTransition() {
-            return true;
-        }
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 7a8b58e..32fda48 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -31,6 +31,7 @@
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -253,4 +254,9 @@
     public View getFocusedChild() {
         return null;
     }
+
+    @VisibleForTesting
+    public DividerType getDividerType() {
+        return mDividerType;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index e31b1d4..3dcb2ac 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -16,11 +16,11 @@
 package com.android.launcher3.statehandlers;
 
 import static android.view.View.VISIBLE;
+import static android.window.flags.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.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 
+import android.content.Context;
 import android.os.Debug;
 import android.util.Log;
 import android.view.View;
@@ -30,14 +30,18 @@
 
 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;
 
@@ -49,7 +53,6 @@
 
     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<>();
 
@@ -61,23 +64,43 @@
     @Nullable
     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 DesktopTaskListenerImpl(this, mLauncher.getDisplayId());
-        SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(mDesktopTaskListener);
+    /** Sets the context and re-registers the System Ui listener */
+    private void setContext(@Nullable Context context) {
+        unregisterSystemUiListener();
+        mContext = context;
+        registerSystemUiListener();
+    }
+
+    /** 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;
     }
@@ -126,6 +149,9 @@
      * it.
      */
     public void setVisibleDesktopTasksCount(int visibleTasksCount) {
+        if (mContext == null) {
+            return;
+        }
         if (DEBUG) {
             Log.d(TAG, "setVisibleDesktopTasksCount: visibleTasksCount=" + visibleTasksCount
                     + " currentValue=" + mVisibleDesktopTasksCount);
@@ -141,7 +167,8 @@
                 notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
             }
 
-            if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher) && wasVisible != isVisible) {
+            if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                    && wasVisible != isVisible) {
                 // TODO: b/333533253 - Remove after flag rollout
                 if (mVisibleDesktopTasksCount > 0) {
                     setLauncherViewsVisibility(View.INVISIBLE);
@@ -160,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);
@@ -185,7 +226,7 @@
                 notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
             }
 
-            if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
+            if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
                 return;
             }
             // TODO: b/333533253 - Clean up after flag rollout
@@ -203,13 +244,16 @@
     }
 
     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) {
@@ -295,22 +339,32 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void setLauncherViewsVisibility(int visibility) {
-        if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
+        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);
         }
@@ -320,7 +374,10 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void markLauncherPaused() {
-        if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
+        if (mContext == null) {
+            return;
+        }
+        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             return;
         }
         if (DEBUG) {
@@ -337,7 +394,10 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void markLauncherResumed() {
-        if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
+        if (mContext == null) {
+            return;
+        }
+        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             return;
         }
         if (DEBUG) {
@@ -353,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 {
         /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt b/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt
new file mode 100644
index 0000000..b8060e1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.view.View
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.app.animation.Interpolators
+import com.android.launcher3.LauncherAnimUtils
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.anim.SpringAnimationBuilder
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+
+/** Animator helper that creates bars animators. */
+object BarsLocationAnimatorHelper {
+
+    private const val FADE_OUT_ANIM_ALPHA_DURATION_MS: Long = 50L
+    private const val FADE_OUT_ANIM_ALPHA_DELAY_MS: Long = 50L
+    private const val FADE_OUT_ANIM_POSITION_DURATION_MS: Long = 100L
+    private const val FADE_IN_ANIM_ALPHA_DURATION_MS: Long = 100L
+
+    // Use STIFFNESS_MEDIUMLOW which is not defined in the API constants
+    private const val FADE_IN_ANIM_POSITION_SPRING_STIFFNESS: Float = 400f
+
+    // During fade out animation we shift the bubble bar 1/80th of the screen width
+    private const val FADE_OUT_ANIM_POSITION_SHIFT: Float = 1 / 80f
+
+    // During fade in animation we shift the bubble bar 1/60th of the screen width
+    private const val FADE_IN_ANIM_POSITION_SHIFT: Float = 1 / 60f
+
+    private val View.screenWidth: Int
+        get() = resources.displayMetrics.widthPixels
+
+    private val View.outShift: Float
+        get() = screenWidth * FADE_OUT_ANIM_POSITION_SHIFT
+
+    private val View.inShiftX: Float
+        get() = screenWidth * FADE_IN_ANIM_POSITION_SHIFT
+
+    /**
+     * Creates out animation for targetView that animates it finalTx and plays targetViewAlphaAnim
+     * to its final value.
+     */
+    private fun createLocationOutAnimator(
+        finalTx: Float,
+        targetViewAlphaAnim: ObjectAnimator,
+        targetView: View,
+    ): Animator {
+        val positionAnim =
+            ObjectAnimator.ofFloat(targetView, VIEW_TRANSLATE_X, finalTx)
+                .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS)
+        positionAnim.interpolator = Interpolators.EMPHASIZED_ACCELERATE
+
+        targetViewAlphaAnim.setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS)
+        targetViewAlphaAnim.startDelay = FADE_OUT_ANIM_ALPHA_DELAY_MS
+
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(positionAnim, targetViewAlphaAnim)
+        return animatorSet
+    }
+
+    /**
+     * Creates in animation for targetView that animates it from startTx to finalTx and plays
+     * targetViewAlphaAnim to its final value.
+     */
+    private fun createLocationInAnimator(
+        startTx: Float,
+        finalTx: Float,
+        targetViewAlphaAnim: ObjectAnimator,
+        targetView: View,
+    ): Animator {
+        targetViewAlphaAnim.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS)
+        val positionAnim: ValueAnimator =
+            SpringAnimationBuilder(targetView.context)
+                .setStartValue(startTx)
+                .setEndValue(finalTx)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+                .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
+                .build(targetView, VIEW_TRANSLATE_X)
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(positionAnim, targetViewAlphaAnim)
+        return animatorSet
+    }
+
+    /** Creates an animator for the bubble bar view in part. */
+    @JvmStatic
+    fun getBubbleBarLocationInAnimator(
+        newLocation: BubbleBarLocation,
+        currentLocation: BubbleBarLocation,
+        distanceFromOtherSide: Float,
+        targetViewAlphaAnim: ObjectAnimator,
+        bubbleBarView: View,
+    ): Animator {
+        val shift: Float = bubbleBarView.outShift
+
+        val onLeft = newLocation.isOnLeft(bubbleBarView.isLayoutRtl)
+        val startTx: Float
+        val finalTx =
+            if (newLocation == currentLocation) {
+                // Animated location matches layout location.
+                0f
+            } else {
+                // We are animating in to a transient location, need to move the bar
+                // accordingly.
+                distanceFromOtherSide * (if (onLeft) -1 else 1)
+            }
+        startTx =
+            if (onLeft) {
+                // Bar will be shown on the left side. Start point is shifted right.
+                finalTx + shift
+            } else {
+                // Bar will be shown on the right side. Start point is shifted left.
+                finalTx - shift
+            }
+        return createLocationInAnimator(startTx, finalTx, targetViewAlphaAnim, bubbleBarView)
+    }
+
+    /** Creates an animator for the bubble bar view out part. */
+    @JvmStatic
+    fun getBubbleBarLocationOutAnimator(
+        bubbleBarView: View,
+        bubbleBarLocation: BubbleBarLocation,
+        targetViewAlphaAnim: ObjectAnimator,
+    ): Animator {
+        val onLeft = bubbleBarLocation.isOnLeft(bubbleBarView.isLayoutRtl)
+        val shift = bubbleBarView.outShift
+        val finalTx = bubbleBarView.translationX + (if (onLeft) -shift else shift)
+        return this.createLocationOutAnimator(finalTx, targetViewAlphaAnim, bubbleBarView)
+    }
+
+    /** Creates a teleport animator for the navigation buttons view. */
+    @JvmStatic
+    fun getTeleportAnimatorForNavButtons(
+        location: BubbleBarLocation,
+        navButtonsView: View,
+        navBarTargetTranslationX: Float,
+    ): Animator {
+        val outShift: Float = navButtonsView.outShift
+        val isNavBarOnRight: Boolean = location.isOnLeft(navButtonsView.isLayoutRtl)
+        val finalOutTx =
+            navButtonsView.translationX + (if (isNavBarOnRight) outShift else -outShift)
+        val fadeout: Animator =
+            createLocationOutAnimator(
+                finalOutTx,
+                ObjectAnimator.ofFloat(navButtonsView, LauncherAnimUtils.VIEW_ALPHA, 0f),
+                navButtonsView,
+            )
+        val inShift: Float = navButtonsView.inShiftX
+        val inStartX = navBarTargetTranslationX + (if (isNavBarOnRight) -inShift else inShift)
+        val fadeIn: Animator =
+            createLocationInAnimator(
+                inStartX,
+                navBarTargetTranslationX,
+                ObjectAnimator.ofFloat(navButtonsView, LauncherAnimUtils.VIEW_ALPHA, 1f),
+                navButtonsView,
+            )
+        val teleportAnimator = AnimatorSet()
+        teleportAnimator.play(fadeout).before(fadeIn)
+        return teleportAnimator
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index 06d9ee6..929e793 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -70,7 +70,6 @@
     @Override
     protected void init(TaskbarControllers taskbarControllers) {
         super.init(taskbarControllers);
-
         mRecentsActivity.setTaskbarUIController(this);
         mRecentsActivity.getStateManager().addStateListener(mStateListener);
     }
@@ -78,6 +77,7 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
+        getRecentsView().setTaskLaunchListener(null);
         mRecentsActivity.setTaskbarUIController(null);
         mRecentsActivity.getStateManager().removeStateListener(mStateListener);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index e4cc6bb..9912c6c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -17,15 +17,17 @@
 
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
+import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
-import com.android.launcher3.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;
@@ -38,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;
 
@@ -45,7 +48,7 @@
  * Handles initialization of the {@link KeyboardQuickSwitchViewController}.
  */
 public final class KeyboardQuickSwitchController implements
-        TaskbarControllers.LoggableTaskbarController {
+        TaskbarControllers.LoggableTaskbarController, TouchController {
 
     @VisibleForTesting
     public static final int MAX_TASKS = 6;
@@ -66,6 +69,7 @@
     private TaskbarControllers mControllers;
 
     @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController;
+    @Nullable private TaskbarOverlayContext mOverlayContext;
 
     private boolean mHasDesktopTask = false;
     private boolean mWasDesktopTaskFilteredOut = false;
@@ -97,7 +101,21 @@
         openQuickSwitchView(-1);
     }
 
+    /**
+     * Opens the view with a filtered list of tasks.
+     * @param taskIdsToExclude A list of tasks to exclude in the opened view.
+     */
+    void openQuickSwitchView(@NonNull Set<Integer> taskIdsToExclude) {
+        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()) {
                 return;
@@ -105,23 +123,25 @@
             // Allow the KQS to be reopened during the close animation to make it more responsive
             closeQuickSwitchView(false);
         }
-        TaskbarOverlayContext overlayContext =
-                mControllers.taskbarOverlayController.requestWindow();
+        mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
+        if (Flags.taskbarOverflow()) {
+            mOverlayContext.getDragLayer().addTouchController(this);
+        }
         KeyboardQuickSwitchView keyboardQuickSwitchView =
-                (KeyboardQuickSwitchView) overlayContext.getLayoutInflater()
+                (KeyboardQuickSwitchView) mOverlayContext.getLayoutInflater()
                         .inflate(
                                 R.layout.keyboard_quick_switch_view,
-                                overlayContext.getDragLayer(),
+                                mOverlayContext.getDragLayer(),
                                 /* attachToRoot= */ false);
         mQuickSwitchViewController = new KeyboardQuickSwitchViewController(
-                mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks);
+                mControllers, mOverlayContext, keyboardQuickSwitchView, mControllerCallbacks);
 
-        DesktopVisibilityController desktopController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
         final boolean onDesktop =
-                desktopController != null && desktopController.areDesktopTasksVisible();
+                mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible();
 
-        if (mModel.isTaskListValid(mTaskListChangeId)) {
+        // TODO(b/368119679) For now we will re-process the task list every time, but this can be
+        // optimized if we have the same set of task ids to exclude.
+        if (mModel.isTaskListValid(mTaskListChangeId) && !Flags.taskbarOverflow()) {
             // 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(
@@ -132,7 +152,8 @@
                             ? 0 : currentFocusedIndex,
                     onDesktop,
                     mHasDesktopTask,
-                    mWasDesktopTaskFilteredOut);
+                    mWasDesktopTaskFilteredOut,
+                    wasOpenedFromTaskbar);
             return;
         }
 
@@ -140,9 +161,9 @@
             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.
@@ -154,15 +175,21 @@
                             ? 0 : currentFocusedIndex,
                     onDesktop,
                     mHasDesktopTask,
-                    mWasDesktopTaskFilteredOut);
+                    mWasDesktopTaskFilteredOut,
+                    wasOpenedFromTaskbar);
         });
     }
 
-    private void processLoadedTasks(List<GroupTask> tasks) {
+    private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
+        return Flags.taskbarOverflow() && taskIdsToExclude.contains(task.task1.key.id);
+    }
+
+    private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
         // Only store MAX_TASK tasks, from most to least recent
         Collections.reverse(tasks);
         mTasks = tasks.stream()
-                .filter(task -> !(task instanceof DesktopTask))
+                .filter(task -> !(task instanceof DesktopTask)
+                        && !shouldExcludeTask(task, taskIdsToExclude))
                 .limit(MAX_TASKS)
                 .collect(Collectors.toList());
 
@@ -180,12 +207,15 @@
                 tasks.size() - (mWasDesktopTaskFilteredOut ? 1 : 0) - MAX_TASKS);
     }
 
-    private void processLoadedTasksOnDesktop(List<GroupTask> tasks) {
+    private void processLoadedTasksOnDesktop(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
         // Find the single desktop task that contains a grouping of desktop tasks
         DesktopTask desktopTask = findDesktopTask(tasks);
 
         if (desktopTask != null) {
-            mTasks = desktopTask.tasks.stream().map(GroupTask::new).collect(Collectors.toList());
+            mTasks = desktopTask.tasks.stream()
+                    .map(GroupTask::new)
+                    .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
+                    .collect(Collectors.toList());
             // All other tasks, apart from the grouped desktop task, are hidden
             mNumHiddenTasks = Math.max(0, tasks.size() - 1);
         } else {
@@ -224,6 +254,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();
@@ -283,6 +334,11 @@
         }
 
         void onCloseComplete() {
+            if (Flags.taskbarOverflow() && mOverlayContext != null) {
+                mOverlayContext.getDragLayer()
+                        .removeTouchController(KeyboardQuickSwitchController.this);
+            }
+            mOverlayContext = null;
             mQuickSwitchViewController = null;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index b4102a9..fc8204a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -58,8 +58,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 {
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 40e77e2..1c8a094 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -19,7 +19,9 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.view.Gravity;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.animation.AnimationUtils;
 import android.window.RemoteTransition;
 
@@ -30,6 +32,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
+import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SlideInRemoteTransition;
@@ -83,7 +87,9 @@
             int currentFocusIndexOverride,
             boolean onDesktop,
             boolean hasDesktopTask,
-            boolean wasDesktopTaskFilteredOut) {
+            boolean wasDesktopTaskFilteredOut,
+            boolean wasOpenedFromTaskbar) {
+        positionView(wasOpenedFromTaskbar);
         mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
         mOnDesktop = onDesktop;
         mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
@@ -98,6 +104,19 @@
                 /* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
     }
 
+    protected void positionView(boolean wasOpenedFromTaskbar) {
+        if (!wasOpenedFromTaskbar) {
+            // Keep the default positioning.
+            return;
+        }
+
+        BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(
+                mKeyboardQuickSwitchView.getLayoutParams());
+        lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
+        lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+        mKeyboardQuickSwitchView.setLayoutParams(lp);
+    }
+
     boolean isCloseAnimationRunning() {
         return mCloseAnimation != null;
     }
@@ -219,6 +238,13 @@
         pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
     }
 
+    /**
+     * @return True if the MotionEvent is over the {@link KeyboardQuickSwitchView}.
+     */
+    protected boolean isEventOverKeyboardQuickSwitch(TaskbarOverlayDragLayer dl, MotionEvent ev) {
+        return dl.isEventOverView(mKeyboardQuickSwitchView, ev);
+    }
+
     class ViewCallbacks {
 
         boolean onKeyUp(int keyCode, KeyEvent event, boolean isRTL, boolean allowTraversal) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 69da7b6..10ff9ac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -15,15 +15,15 @@
  */
 package com.android.launcher3.taskbar;
 
+import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+
 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.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
-import android.graphics.Rect;
 import android.window.RemoteTransition;
 
 import androidx.annotation.NonNull;
@@ -38,20 +38,20 @@
 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 +128,8 @@
 
     @Override
     protected void onDestroy() {
+        onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */);
         super.onDestroy();
-        onLauncherVisibilityChanged(false);
         mTaskbarLauncherStateController.onDestroy();
 
         mLauncher.setTaskbarUIController(null);
@@ -137,11 +137,6 @@
         mHomeState.removeListener(mVisibilityChangeListener);
     }
 
-    /** Returns {@code true} if launcher is currently presenting the home screen. */
-    public boolean isOnHome() {
-        return mTaskbarLauncherStateController.isOnHome();
-    }
-
     private void onInAppDisplayProgressChanged() {
         if (mControllers != null) {
             // Update our shared state so we can restore it if taskbar gets recreated.
@@ -165,6 +160,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.
      *
@@ -185,35 +190,20 @@
     }
 
     /**
-     * Returns the bounds of launcher's hotseat.
-     */
-    public void getHotseatBounds(Rect hotseatBoundsOut) {
-        DeviceProfile launcherDP = mLauncher.getDeviceProfile();
-        if (launcherDP.isQsbInline) {
-            // Not currently supported.
-            hotseatBoundsOut.setEmpty();
-            return;
-        }
-        int left = (launcherDP.widthPx - launcherDP.getHotseatWidthPx()
-                - mLauncher.getHotseat().getUnusedHorizontalSpace()) / 2;
-        int right = left + launcherDP.getHotseatWidthPx();
-        int bottom = launcherDP.getHotseatLayoutPadding(mLauncher).bottom;
-        int top = bottom - launcherDP.hotseatCellHeightPx;
-        hotseatBoundsOut.set(left, top, right, bottom);
-    }
-
-    /**
      * Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
      */
     @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
@@ -224,7 +214,7 @@
 
     @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.
@@ -235,17 +225,14 @@
             return null;
         }
 
-        DesktopVisibilityController desktopController =
-                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-        if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher)
-                && 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 || mControllers == null) {
+        if (fromInitOrDestroy) {
             duration = 0;
         }
         return mTaskbarLauncherStateController.applyState(duration, startAnimation);
@@ -481,4 +468,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 a979d58..895535e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.LauncherAnimUtils.ROTATION_DRAWABLE_PERCENT;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
@@ -48,7 +49,9 @@
 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.enableBubbleBarInPersistentTaskBar;
 
+import android.animation.Animator;
 import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
 import android.annotation.DrawableRes;
@@ -91,6 +94,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,6 +110,7 @@
 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;
@@ -115,7 +120,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();
 
@@ -172,6 +178,9 @@
     /** Color to use for navigation bar 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(
@@ -1168,6 +1177,82 @@
         mHitboxExtender.onAnimationProgressToOverview(alignment);
     }
 
+    /** Adjusts navigation buttons layout accordingly to the bubble bar position. */
+    @Override
+    public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
+        cancelExistingNavBarAnimation();
+        mBubbleBarTargetLocation = location;
+        mNavButtonContainer.setTranslationX(getNavBarTranslationX(location));
+        mNavButtonContainer.setAlpha(1);
+    }
+
+    /** Animates navigation buttons accordingly to the bubble bar position. */
+    @Override
+    public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
+        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 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 onTaskbarLayoutChanged() {
+        if (mControllers.taskbarViewController.getIconLayoutBounds().isEmpty()) return;
+        if (enableBubbleBarInPersistentTaskBar()
+                && mControllers.bubbleControllers.isPresent()) {
+            if (mBubbleBarTargetLocation == null) {
+                // only set bubble bar location if it was not set before, e.g. at device boot
+                mBubbleBarTargetLocation = mControllers.bubbleControllers.get()
+                        .bubbleBarViewController.getBubbleBarLocation();
+            }
+            onBubbleBarLocationUpdated(mBubbleBarTargetLocation);
+        }
+    }
+
     private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback {
         @Override
         public void onVisibilityChanged(boolean isVisible) {
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/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index fabf3a5..7273fac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -208,11 +208,8 @@
      * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the stashed handle
      * shape and size. When stashed, the shape is a thin rounded pill. When unstashed, the shape
      * morphs into the size of where the taskbar icons will be.
-     *
-     * @param taskbarToHotseatOffsets A Rect of offsets used to transform the bounds of the
-     *                                stashed handle to wrap around the hotseat items.
      */
-    public Animator createRevealAnimToIsStashed(boolean isStashed, Rect taskbarToHotseatOffsets) {
+    public Animator createRevealAnimToIsStashed(boolean isStashed) {
         Rect visualBounds = mControllers.taskbarViewController.getIconLayoutVisualBounds();
         float startRadius = mStashedHandleRadius;
 
@@ -223,13 +220,6 @@
             visualBounds.bottom += heightDiff;
 
             startRadius = visualBounds.height() / 2f;
-
-            // We use these offsets to create a larger stashed handle to wrap around the items
-            // of the hotseat. This is only used for certain animations.
-            visualBounds.top +=  taskbarToHotseatOffsets.top;
-            visualBounds.bottom += taskbarToHotseatOffsets.bottom;
-            visualBounds.left += taskbarToHotseatOffsets.left;
-            visualBounds.right += taskbarToHotseatOffsets.right;
         }
 
         final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b10a59f..a59445b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -29,6 +29,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.taskbarOverflow;
 import static com.android.launcher3.Utilities.calculateTextHeight;
 import static com.android.launcher3.Utilities.isRunningInTestHarness;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
@@ -37,6 +38,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;
@@ -100,11 +102,13 @@
 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;
@@ -128,11 +132,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;
@@ -140,7 +144,6 @@
 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;
@@ -223,7 +226,8 @@
     public TaskbarActivityContext(Context windowContext,
             @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp,
             TaskbarNavButtonController buttonController, ScopedUnfoldTransitionProgressProvider
-            unfoldTransitionProgressProvider) {
+            unfoldTransitionProgressProvider,
+            @NonNull DesktopVisibilityController desktopVisibilityController) {
         super(windowContext);
 
         mNavigationBarPanelContext = navigationBarPanelContext;
@@ -276,9 +280,11 @@
         BubbleBarController.onTaskbarRecreated();
         if (BubbleBarController.isBubbleBarEnabled() && 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);
@@ -296,6 +302,7 @@
                             () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
                     new BubblePinController(this, mDragLayer,
                             () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+                    bubbleBarSwipeController,
                     new BubbleCreator(this)
             ));
         }
@@ -336,17 +343,13 @@
                 new VoiceInteractionWindowController(this),
                 new TaskbarTranslationController(this),
                 new TaskbarSpringOnStashController(this),
-                new TaskbarRecentAppsController(
-                        this,
-                        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)),
+                new TaskbarPinningController(this),
                 bubbleControllersOptional,
-                new TaskbarDesktopModeController(
-                        LauncherActivityInterface.INSTANCE::getDesktopVisibilityController));
+                new TaskbarDesktopModeController(desktopVisibilityController));
+
         mLauncherPrefs = LauncherPrefs.get(this);
     }
 
@@ -363,38 +366,6 @@
     }
 
     /**
-     * Calculate the offsets needed to transform the transient taskbar bounds to the hotseat bounds.
-     * @return The offsets will be stored in a Rect
-     */
-    public Rect calculateTaskbarToHotseatOffsets(Rect hotseatBounds) {
-        Rect taskbar = getTransientTaskbarBounds();
-        Rect offsets = new Rect();
-
-        offsets.left = hotseatBounds.left - taskbar.left;
-        offsets.right = hotseatBounds.right - taskbar.right;
-
-        int heightDiff = hotseatBounds.height() - taskbar.height();
-        offsets.top = (taskbar.height() - heightDiff) / 2;
-
-        int gleanedTaskbarPadding = (mDeviceProfile.taskbarHeight
-                - getTransientTaskbarBounds().height()) / 2;
-        offsets.left -= gleanedTaskbarPadding;
-        offsets.top -= gleanedTaskbarPadding;
-        offsets.right += gleanedTaskbarPadding;
-
-        // Bottom is relative to the bottom of layout, so we can calculate it with padding included.
-        offsets.bottom = (hotseatBounds.height() - taskbar.height()) / 2;
-
-        // Update bounds in taskbar background
-        if (hotseatBounds.isEmpty()) {
-            mDragLayer.getTaskbarToHotseatOffsetRect().setEmpty();
-        } else {
-            mDragLayer.getTaskbarToHotseatOffsetRect().set(offsets);
-        }
-        return offsets;
-    }
-
-    /**
      * Copy the original DeviceProfile, match the number of hotseat icons and qsb width and update
      * the icon size
      */
@@ -690,6 +661,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;
     }
@@ -1208,14 +1184,26 @@
         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();
+
+        if (taskbarOverflow()) {
+            mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
+        }
+
         if (tag instanceof GroupTask groupTask) {
-            handleGroupTaskLaunch(groupTask, /* remoteTransition = */ null,
-                    DisplayController.isInDesktopMode(this));
+            handleGroupTaskLaunch(
+                    groupTask,
+                    /* remoteTransition= */ null,
+                    areDesktopTasksVisible());
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
         } else if (tag instanceof FolderInfo) {
             // Tapping an expandable folder icon on Taskbar
@@ -1249,7 +1237,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()) {
@@ -1546,10 +1535,14 @@
 
     /**
      * 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();
@@ -1683,7 +1676,7 @@
                 duration);
 
         View allAppsButton = mControllers.taskbarViewController.getAllAppsButtonView();
-        if (allAppsButton != null && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get()) {
+        if (allAppsButton != null && !FeatureFlags.enableAllAppsButtonInHotseat()) {
             ValueAnimator alphaOverride = ValueAnimator.ofFloat(0, 1);
             alphaOverride.setDuration(duration);
             alphaOverride.addUpdateListener(a -> {
@@ -1697,18 +1690,6 @@
     }
 
     /**
-     * Returns the bounds of launcher's hotseat (if exists).
-     */
-    public void getHotseatBounds(Rect hotseatBoundsOut) {
-        TaskbarUIController uiController = mControllers.uiController;
-        if (uiController instanceof LauncherTaskbarUIController launcherController) {
-            launcherController.getHotseatBounds(hotseatBoundsOut);
-        } else {
-            hotseatBoundsOut.setEmpty();
-        }
-    }
-
-    /**
      * Called when we determine the touchable region.
      *
      * @param exclude {@code true} then the magnification region computation will omit the window.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index 4ac7514..c0e921e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -21,7 +21,6 @@
 import android.graphics.Color
 import android.graphics.Paint
 import android.graphics.Path
-import android.graphics.Rect
 import android.graphics.RectF
 import com.android.app.animation.Interpolators
 import com.android.internal.policy.ScreenDecorationsUtils
@@ -60,9 +59,6 @@
     var translationYForSwipe = 0f
     var translationYForStash = 0f
 
-    // When not empty, we can use this to transform transient taskbar background to hotseat bounds.
-    val taskbarToHotseatOffsetRect = Rect()
-
     private val transientBackgroundBounds = context.transientTaskbarBounds
 
     private val shadowAlpha: Float
@@ -102,7 +98,7 @@
             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
         }
 
-        if (DisplayController.isInDesktopMode(context)) {
+        if (context.areDesktopTasksVisible()) {
             fullCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
             cornerRadius = fullCornerRadius
         } else {
@@ -200,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 =
@@ -230,12 +226,6 @@
         val radius = newBackgroundHeight / 2f
         val bottomMarginProgress = bottomMargin * ((1f - progress) / 2f)
 
-        // Used to transform the background so that it wraps around the items on the hotseat.
-        val hotseatOffsetLeft = taskbarToHotseatOffsetRect.left * progress
-        val hotseatOffsetTop = taskbarToHotseatOffsetRect.top * progress
-        val hotseatOffsetRight = taskbarToHotseatOffsetRect.right * progress
-        val hotseatOffsetBottom = taskbarToHotseatOffsetRect.bottom * progress
-
         // Aligns the bottom with the bottom of the stashed handle.
         val bottom =
             canvas.height - bottomMargin +
@@ -245,7 +235,7 @@
                 -mapRange(
                     1f - progress,
                     0f,
-                    if (isAnimatingPinning) 0f else stashedHandleHeight / 2f
+                    if (isAnimatingPinning) 0f else stashedHandleHeight / 2f,
                 )
 
         // Draw shadow.
@@ -255,15 +245,15 @@
             shadowBlur,
             0f,
             keyShadowDistance,
-            setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
+            setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)),
         )
         strokePaint.alpha = (paint.alpha * strokeAlpha) / 255
 
         lastDrawnTransientRect.set(
-            transientBackgroundBounds.left + halfWidthDelta + hotseatOffsetLeft,
-            bottom - newBackgroundHeight + hotseatOffsetTop,
-            transientBackgroundBounds.right - halfWidthDelta + hotseatOffsetRight,
-            bottom + hotseatOffsetBottom
+            transientBackgroundBounds.left + halfWidthDelta,
+            bottom - newBackgroundHeight,
+            transientBackgroundBounds.right - halfWidthDelta,
+            bottom
         )
         val horizontalInset = fullWidth * widthInsetPercentage
         lastDrawnTransientRect.inset(horizontalInset, 0f)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 34ab9f0..5974675 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -25,8 +25,8 @@
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
-import com.android.launcher3.util.DisplayController;
 import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -194,13 +194,17 @@
                 voiceInteractionWindowController
         };
 
-        if (DisplayController.isInDesktopMode(taskbarActivityContext)) {
+        if (taskbarDesktopModeController.getAreDesktopTasksVisible()) {
             mCornerRoundness.updateValue(taskbarDesktopModeController.getTaskbarCornerRoundness(
                     mSharedState.showCornerRadiusInDesktopMode));
         } else {
             mCornerRoundness.updateValue(TaskbarBackgroundRenderer.MAX_ROUNDNESS);
         }
+        onPostInit();
+    }
 
+    @VisibleForTesting
+    public void onPostInit() {
         mAreAllControllersInitialized = true;
         for (Runnable postInitCallback : mPostInitCallbacks) {
             postInitCallback.run();
@@ -216,7 +220,11 @@
         uiController = newUiController;
         uiController.init(this);
         uiController.updateStateForSysuiFlags(mSharedState.sysuiStateFlags);
-
+        bubbleControllers.ifPresent(bubbleControllers -> {
+            BubbleBarLocation location =
+                    bubbleControllers.bubbleBarViewController.getBubbleBarLocation();
+            uiController.onBubbleBarLocationUpdated(location);
+        });
         // Notify that the ui controller has changed
         navbarButtonsViewController.onUiControllerChanged();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
index a376531..47a35c5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
@@ -22,18 +22,18 @@
 
 /** Handles Taskbar in Desktop Windowing mode. */
 class TaskbarDesktopModeController(
-    private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?
+    private val desktopVisibilityController: DesktopVisibilityController
 ) : TaskbarDesktopModeListener {
     private lateinit var taskbarControllers: TaskbarControllers
     private lateinit var taskbarSharedState: TaskbarSharedState
 
-    private val desktopVisibilityController: DesktopVisibilityController?
-        get() = desktopVisibilityControllerProvider()
+    val areDesktopTasksVisible: Boolean
+        get() = desktopVisibilityController.areDesktopTasksVisible()
 
     fun init(controllers: TaskbarControllers, sharedState: TaskbarSharedState) {
         taskbarControllers = controllers
         taskbarSharedState = sharedState
-        desktopVisibilityController?.registerTaskbarDesktopModeListener(this)
+        desktopVisibilityController.registerTaskbarDesktopModeListener(this)
     }
 
     override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {
@@ -50,5 +50,5 @@
         }
     }
 
-    fun onDestroy() = desktopVisibilityController?.unregisterTaskbarDesktopModeListener(this)
+    fun onDestroy() = desktopVisibilityController.unregisterTaskbarDesktopModeListener(this)
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 5bbf4b2..fc307b2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -74,14 +74,12 @@
 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;
@@ -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 a090956..e16c76d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -22,7 +22,6 @@
 
 import android.content.Context;
 import android.graphics.Canvas;
-import android.graphics.Rect;
 import android.graphics.RectF;
 import android.media.permission.SafeCloseable;
 import android.util.AttributeSet;
@@ -100,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);
@@ -109,7 +108,7 @@
 
     public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) {
         mControllerCallbacks = callbacks;
-        mBackgroundRenderer.updateStashedHandleWidth(mActivity, getResources());
+        mBackgroundRenderer.updateStashedHandleWidth(mContainer, getResources());
         recreateControllers();
     }
 
@@ -260,11 +259,6 @@
         return mBackgroundRenderer.getLastDrawnTransientRect();
     }
 
-    /** Returns the rect used to transform transient taskbar to the hotseat */
-    public Rect getTaskbarToHotseatOffsetRect() {
-        return mBackgroundRenderer.getTaskbarToHotseatOffsetRect();
-    }
-
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
@@ -275,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/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 221504d..685c109 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -147,7 +147,11 @@
                 defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
             }
         }
-        if (taskbarStashController.isInApp || taskbarStashController.isInOverview) {
+        if (
+            taskbarStashController.isInApp ||
+                taskbarStashController.isInOverview ||
+                DisplayController.showLockedTaskbarOnHome(context)
+        ) {
             // only add the taskbar touch region if not on home
             val bottom = windowLayoutParams.height
             val top = bottom - taskbarTouchableHeight
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 4188a0f..065646a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -16,10 +16,17 @@
 package com.android.launcher3.taskbar;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+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.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.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE;
@@ -38,8 +45,11 @@
 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;
@@ -57,6 +67,7 @@
 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;
@@ -145,7 +156,8 @@
     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;
@@ -163,6 +175,8 @@
 
     private boolean mShouldDelayLauncherStateAnim;
 
+    private @Nullable BubbleBarLocation mBubbleBarLocation;
+
     // We skip any view synchronizations during init/destroy.
     private boolean mCanSyncViews;
 
@@ -175,11 +189,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);
                 }
             };
 
@@ -242,7 +258,7 @@
                 .getTaskbarBackgroundAlpha();
         mTaskbarAlpha = mControllers.taskbarDragLayerController.getTaskbarAlpha();
         mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness();
-        mIconAlphaForHome = mControllers.taskbarViewController
+        mTaskbarAlphaForHome = mControllers.taskbarViewController
                 .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME);
 
         resetIconAlignment();
@@ -266,7 +282,7 @@
 
         mIconAlignment.finishAnimation();
 
-        mLauncher.getHotseat().setIconsAlpha(1f);
+        mLauncher.getHotseat().setIconsAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
         mLauncher.getStateManager().removeStateListener(mStateListener);
 
         mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode();
@@ -295,6 +311,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));
 
@@ -375,7 +392,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(),
@@ -441,11 +458,6 @@
         return animator;
     }
 
-    /** Returns {@code true} if launcher is currently presenting the home screen. */
-    public boolean isOnHome() {
-        return isInLauncher() && mLauncherState == LauncherState.NORMAL;
-    }
-
     private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
         final boolean isInLauncher = isInLauncher();
         final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat();
@@ -457,9 +469,11 @@
                     + ", toAlignment: " + toAlignment);
         }
         mControllers.bubbleControllers.ifPresent(controllers -> {
-            // Show the bubble bar when on launcher home or in overview.
+            // Show the bubble bar when on launcher home (hotseat icons visible) or in overview
             boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
-            controllers.bubbleStashController.setBubblesShowingOnHome(isOnHome());
+            boolean hotseatIconsVisible = isInLauncher && mLauncherState.areElementsVisible(
+                    mLauncher, HOTSEAT_ICONS);
+            controllers.bubbleStashController.setBubblesShowingOnHome(hotseatIconsVisible);
             controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview);
         });
 
@@ -593,7 +607,8 @@
 
         float cornerRoundness = isInLauncher ? 0 : 1;
 
-        if (DisplayController.isInDesktopMode(mLauncher) && mControllers.getSharedState() != null) {
+        if (mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()
+                && mControllers.getSharedState() != null) {
             cornerRoundness =
                     mControllers.taskbarDesktopModeController.getTaskbarCornerRoundness(
                             mControllers.getSharedState().showCornerRadiusInDesktopMode);
@@ -660,6 +675,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);
     }
 
@@ -671,8 +689,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;
@@ -703,14 +720,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);
                     }
                 }
             });
@@ -739,6 +759,33 @@
         }
     }
 
+    protected void stashHotseat(boolean stash) {
+        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.
      */
@@ -748,7 +795,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);
@@ -756,8 +803,10 @@
         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()) {
@@ -767,12 +816,20 @@
         }
     }
 
-    private void updateIconAlphaForHome(float alpha) {
+    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);
@@ -780,12 +837,81 @@
          * 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(BubbleBarLocation location, boolean animate) {
+        DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
+        if (mBubbleBarLocation == location) return;
+        mBubbleBarLocation = location;
+        if (!deviceProfile.shouldAdjustHotseatOnBubblesLocationUpdate(
+                mControllers.taskbarActivityContext)) {
+            return;
+        }
+        int targetX = 0;
+        if (mBubbleBarLocation != null) {
+            boolean isBubblesOnLeft = location.isOnLeft(isRtl(mLauncher.getResources()));
+            targetX = deviceProfile.getHotseatTranslationXForBubbleBar(/* isNavbarOnRight= */
+                    isBubblesOnLeft);
+        }
+        updateHotseatAndQsbTranslationX(targetX, animate);
+    }
+
+    private void updateHotseatAndQsbTranslationX(float targetValue, boolean animate) {
+        // cancel existing animation
+        if (mHotseatTranslationXAnimation != null) {
+            mHotseatTranslationXAnimation.cancel();
+        }
+        Runnable alignTaskbar = new Runnable() {
+            @Override
+            public void run() {
+                // We only need to align the task bar when on launcher home screen
+                if (mControllers.taskbarStashController.isOnHome()) {
+                    DeviceProfile dp = mLauncher.getDeviceProfile();
+                    mControllers.taskbarViewController
+                            .setLauncherIconAlignment(/* alignmentRatio = */ 1, dp);
+                }
+            }
+        };
+        Hotseat hotseat = mLauncher.getHotseat();
+        AnimatorSet translationXAnimation = new AnimatorSet();
+        MultiProperty iconsTranslationX = hotseat.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) {
+            alignTaskbar.run();
+            return;
+        }
+        mHotseatTranslationXAnimation = translationXAnimation;
+        translationXAnimation.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS);
+        translationXAnimation.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
+        translationXAnimation.setInterpolator(Interpolators.EMPHASIZED);
+        translationXAnimation.addListener(AnimatorListeners.forEndCallback(alignTaskbar));
+        translationXAnimation.start();
+    }
+
+
     private final class TaskBarRecentsAnimationListener implements
             RecentsAnimationCallbacks.RecentsAnimationListener {
         private final RecentsAnimationCallbacks mCallbacks;
@@ -870,7 +996,7 @@
         pw.println(String.format(
                 "%s\tmTaskbarBackgroundAlpha=%.2f", prefix, mTaskbarBackgroundAlpha.value));
         pw.println(String.format(
-                "%s\tmIconAlphaForHome=%.2f", prefix, mIconAlphaForHome.getValue()));
+                "%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)));
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 8ecac96..78e7b47 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -60,6 +60,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;
@@ -209,12 +211,14 @@
                 }
             };
 
+    @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);
         mContext = context.createWindowContext(display,
@@ -224,6 +228,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,6 +248,7 @@
                 context,
                 navCallbacks,
                 SystemUiProxy.INSTANCE.get(mContext),
+                ContextualEduStatsManager.INSTANCE.get(mContext),
                 new Handler(),
                 AssistUtils.newInstance(mContext));
         mComponentCallbacks = new ComponentCallbacks() {
@@ -470,7 +476,7 @@
             if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) {
                 mTaskbarActivityContext = new TaskbarActivityContext(mContext,
                         mNavigationBarPanelContext, dp, mNavButtonController,
-                        mUnfoldProgressProvider);
+                        mUnfoldProgressProvider, mDesktopVisibilityController);
             } else {
                 mTaskbarActivityContext.updateDeviceProfile(dp);
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 5024cd8..bdefea6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -250,6 +250,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..15c35b6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -45,12 +45,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.systemui.contextualeducation.GestureType;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 
 import java.io.PrintWriter;
@@ -109,6 +111,7 @@
     private final Context mContext;
     private final TaskbarNavButtonCallbacks mCallbacks;
     private final SystemUiProxy mSystemUiProxy;
+    private final ContextualEduStatsManager mContextualEduStatsManager;
     private final Handler mHandler;
     private final AssistUtils mAssistUtils;
     @Nullable private StatsLogManager mStatsLogManager;
@@ -119,11 +122,13 @@
             Context context,
             TaskbarNavButtonCallbacks callbacks,
             SystemUiProxy systemUiProxy,
+            ContextualEduStatsManager contextualEduStatsManager,
             Handler handler,
             AssistUtils assistUtils) {
         mContext = context;
         mCallbacks = callbacks;
         mSystemUiProxy = systemUiProxy;
+        mContextualEduStatsManager = contextualEduStatsManager;
         mHandler = handler;
         mAssistUtils = assistUtils;
     }
@@ -137,14 +142,20 @@
         switch (buttonType) {
             case BUTTON_BACK:
                 logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
+                mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
+                        GestureType.BACK);
                 executeBack();
                 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:
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index 6c9cc64..1867cd0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -32,10 +32,8 @@
 import java.io.PrintWriter
 
 /** 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 +56,7 @@
                     return
                 }
                 val shouldPinTaskbar =
-                    if (isInDesktopModeProvider()) {
+                    if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) {
                         !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
                     } else {
                         !launcherPrefs.get(TASKBAR_PINNING)
@@ -119,7 +117,7 @@
             dragLayerController.taskbarBackgroundProgress.animateToValue(animateToValue),
             taskbarViewController.taskbarIconTranslationYForPinning.animateToValue(animateToValue),
             taskbarViewController.taskbarIconScaleForPinning.animateToValue(animateToValue),
-            taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue)
+            taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue),
         )
 
         animatorSet.interpolator = Interpolators.EMPHASIZED
@@ -134,10 +132,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 332eb95..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,9 +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;
@@ -79,6 +84,7 @@
     // Initialized in init.
     private TaskbarControllers mControllers;
     private boolean mAllowInitialSplitSelection;
+    private AppInfo[] mAppInfosList;
 
     public TaskbarPopupController(TaskbarActivityContext context) {
         mContext = context;
@@ -189,12 +195,16 @@
         // here will reflect in the popup
         ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>();
         shortcuts.add(APP_INFO);
-        if (!mControllers.taskbarRecentAppsController.isInDesktopMode()) {
+        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();
     }
 
@@ -258,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 737d031..57d4dbb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -16,18 +16,17 @@
 package com.android.launcher3.taskbar
 
 import android.content.Context
+import android.window.flags.DesktopModeFlags
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.TaskItemInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
-import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
 import com.android.launcher3.util.CancellableTask
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
-import com.android.wm.shell.shared.desktopmode.DesktopModeFlags
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import java.io.PrintWriter
 
@@ -36,17 +35,12 @@
  * - When in Fullscreen mode: show the N most recent Tasks
  * - When in Desktop Mode: show the currently running (open) Tasks
  */
-class TaskbarRecentAppsController(
-    context: Context,
-    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 {
 
     var canShowRunningApps =
         DesktopModeStatus.canEnterDesktopMode(context) &&
-            DesktopModeFlags.TASKBAR_RUNNING_APPS.isEnabled(context)
+            DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue
         @VisibleForTesting
         set(isEnabledFromTest) {
             field = isEnabledFromTest
@@ -78,19 +72,16 @@
     var shownTasks: List<GroupTask> = emptyList()
         private set
 
-    private val desktopVisibilityController: DesktopVisibilityController?
-        get() = desktopVisibilityControllerProvider()
-
-    val isInDesktopMode: Boolean
-        get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
-
     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()
@@ -102,7 +93,10 @@
          * 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()
@@ -124,7 +118,7 @@
         controllers = taskbarControllers
         if (canShowRunningApps || canShowRecentApps) {
             recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
-            reloadRecentTasksIfNeeded()
+            controllers.runAfterInit { reloadRecentTasksIfNeeded() }
         }
     }
 
@@ -137,8 +131,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()
@@ -150,11 +146,11 @@
                 .filter { itemInfo -> !itemInfo.isPredictedItem }
                 .toMutableList()
 
-        if (isInDesktopMode && canShowRunningApps) {
+        if (areDesktopTasksVisible && canShowRunningApps) {
             shownHotseatItems =
                 updateHotseatItemsFromRunningTasks(
                     getOrderedAndWrappedDesktopTasks(),
-                    shownHotseatItems
+                    shownHotseatItems,
                 )
         }
 
@@ -199,7 +195,7 @@
         val oldShownTasks = shownTasks
         orderedRunningTaskIds = updateOrderedRunningTaskIds()
         shownTasks =
-            if (isInDesktopMode) {
+            if (controllers.taskbarDesktopModeController.areDesktopTasksVisible) {
                 computeShownRunningTasks()
             } else {
                 computeShownRecentTasks()
@@ -281,7 +277,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 ->
@@ -296,7 +292,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 2370dfd..bf086b4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -27,6 +27,8 @@
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.util.DisplayController;
@@ -77,7 +79,7 @@
     public void onTaskbarVisibilityChanged(int visibility) {
         mTaskbarVisible = visibility == VISIBLE;
         if (shouldShowScrim()) {
-            showScrim(true, getScrimAlpha(), false /* skipAnim */);
+            showScrim(true, computeScrimAlpha(), false /* skipAnim */);
         } else if (mScrimView.getScrimAlpha() > 0f) {
             showScrim(false, 0, false /* skipAnim */);
         }
@@ -96,7 +98,7 @@
             return;
         }
         mSysUiStateFlags = stateFlags;
-        showScrim(shouldShowScrim(), getScrimAlpha(), skipAnim);
+        showScrim(shouldShowScrim(), computeScrimAlpha(), skipAnim);
     }
 
     private boolean shouldShowScrim() {
@@ -115,10 +117,11 @@
         return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible()
                 && !isShadeVisible
                 && !mControllers.taskbarStashController.isStashed()
-                && (mTaskbarVisible || showScrimForBubbles);
+                && (mTaskbarVisible || showScrimForBubbles)
+                && !mControllers.taskbarStashController.isHiddenForBubbles();
     }
 
-    private float getScrimAlpha() {
+    private float computeScrimAlpha() {
         final boolean isPersistentTaskBarVisible =
                 mTaskbarVisible && !DisplayController.isTransientTaskbar(mScrimView.getContext());
         final boolean manageMenuExpanded =
@@ -139,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);
@@ -166,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/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index d07b011..6222e53 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -42,7 +42,6 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.app.RemoteAction;
-import android.graphics.Rect;
 import android.graphics.drawable.Icon;
 import android.os.SystemClock;
 import android.util.Log;
@@ -62,11 +61,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;
 
@@ -84,6 +82,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
@@ -96,6 +101,9 @@
     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
 
     // If any of these flags are enabled, isInApp should return true.
     private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -117,26 +125,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.
@@ -157,7 +169,7 @@
     /**
      * How long the icon/stash handle alpha animation plays.
      */
-    public static final long TASKBAR_STASH_ALPHA_DURATION = 50;
+    public static final long TRANSIENT_TASKBAR_STASH_ALPHA_DURATION = 50;
 
     /**
      * How long to delay the icon/stash handle alpha for the home to app taskbar animation.
@@ -202,7 +214,6 @@
      * by not scaling the height of the taskbar background.
      */
     private static final int TRANSITION_UNSTASH_SUW_MANUAL = 3;
-    private static final Rect EMPTY_RECT = new Rect();
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
@@ -247,7 +258,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
@@ -390,6 +401,16 @@
         return mIsStashed;
     }
 
+    /** Sets the hotseat stashed. */
+    public void stashHotseat(boolean stash) {
+        mControllers.uiController.stashHotseat(stash);
+    }
+
+    /** Instantly un-stashes the hotseat. */
+    public void unStashHotseatInstantly() {
+        mControllers.uiController.unStashHotseatInstantly();
+    }
+
     /**
      * Returns whether the taskbar should be stashed in apps (e.g. user long pressed to stash).
      */
@@ -429,6 +450,16 @@
         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.
      */
@@ -491,9 +522,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);
     }
 
     /**
@@ -501,28 +540,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);
@@ -576,6 +634,7 @@
                 /* isStashed= */ mActivity.isPhoneMode(),
                 placeholderDuration,
                 TRANSITION_UNSTASH_SUW_MANUAL,
+                /* skipTaskbarBackgroundDelay */ false,
                 /* jankTag= */ "SUW_MANUAL");
         animation.addListener(AnimatorListeners.forEndCallback(
                 () -> mControllers.taskbarViewController.setDeferUpdatesForSUW(false)));
@@ -585,13 +644,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");
@@ -629,7 +689,8 @@
         }
 
         if (isTransientTaskbar) {
-            createTransientAnimToIsStashed(mAnimator, isStashed, duration, animationType);
+            createTransientAnimToIsStashed(mAnimator, isStashed, duration,
+                    shouldDelayBackground, animationType);
         } else {
             createAnimToIsStashed(mAnimator, isStashed, duration, stashTranslation, animationType);
         }
@@ -719,7 +780,7 @@
         }
 
         fullLengthAnimatorSet.play(mControllers.stashedHandleViewController
-                .createRevealAnimToIsStashed(isStashed, EMPTY_RECT));
+                .createRevealAnimToIsStashed(isStashed));
         // Return the stashed handle to its default scale in case it was changed as part of the
         // feedforward hint. Note that the reveal animation above also visually scales it.
         fullLengthAnimatorSet.play(mTaskbarStashedHandleHintScale.animateToValue(1f));
@@ -735,7 +796,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;
@@ -749,14 +810,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) {
@@ -769,24 +830,14 @@
             }
         }
 
-
-        Rect taskbarToHotseatOffsets = new Rect();
-        if (enableScalingRevealHomeAnimation() && animationType == TRANSITION_HOME_TO_APP) {
-            Rect hotseatRect = new Rect();
-            mActivity.getHotseatBounds(hotseatRect);
-
-            // Calculate and store offsets so that we can sync with the taskbar stashed handle
-            taskbarToHotseatOffsets.set(
-                    mActivity.calculateTaskbarToHotseatOffsets(hotseatRect));
-            as.addListener(AnimatorListeners.forEndCallback(
-                    () -> mActivity.calculateTaskbarToHotseatOffsets(EMPTY_RECT)));
-        }
-
         play(as, mTaskbarStashedHandleAlpha.animateToValue(stashedHandleAlphaTarget),
                 backgroundAndHandleAlphaStartDelay,
                 backgroundAndHandleAlphaDuration, LINEAR);
 
-        if (enableScalingRevealHomeAnimation() && !isStashed) {
+
+        if (enableScalingRevealHomeAnimation()
+                && !isStashed
+                && shouldDelayBackground) {
             play(as, getTaskbarBackgroundAnimatorWhenNotGoingHome(duration),
                     0, 0, LINEAR);
             as.addListener(AnimatorListeners.forEndCallback(
@@ -827,12 +878,10 @@
         }
 
         mControllers.taskbarViewController.addRevealAnimToIsStashed(skippable, isStashed, duration,
-                EMPHASIZED, animationType == TRANSITION_UNSTASH_SUW_MANUAL,
-                animationType == TRANSITION_HOME_TO_APP);
+                EMPHASIZED, animationType == TRANSITION_UNSTASH_SUW_MANUAL);
 
         play(skippable, mControllers.stashedHandleViewController
-                .createRevealAnimToIsStashed(isStashed, taskbarToHotseatOffsets), 0, duration,
-                EMPHASIZED);
+                .createRevealAnimToIsStashed(isStashed), 0, duration, EMPHASIZED);
 
         // Return the stashed handle to its default scale in case it was changed as part of the
         // feedforward hint. Note that the reveal animation above also visually scales it.
@@ -913,7 +962,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 =
@@ -925,9 +974,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);
+            }
         });
     }
 
@@ -957,13 +1013,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();
         }
     }
 
@@ -981,6 +1053,9 @@
      */
     @Nullable
     public Animator createApplyStateAnimator(long duration) {
+        if (mActivity.isPhoneMode()) {
+            return null;
+        }
         return mStatePropertyHolder.createSetStateAnimator(mState, duration);
     }
 
@@ -1013,7 +1088,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;
@@ -1026,10 +1102,6 @@
 
     /** Called when some system ui state has changed. (See SYSUI_STATE_... in QuickstepContract) */
     public void updateStateForSysuiFlags(long systemUiStateFlags, boolean skipAnim) {
-        if (mActivity.isPhoneMode()) {
-            return;
-        }
-
         long animDuration = TASKBAR_STASH_DURATION;
         long startDelay = 0;
 
@@ -1073,16 +1145,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;
         }
 
@@ -1094,6 +1165,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.
      *
@@ -1180,6 +1266,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));
     }
@@ -1213,7 +1305,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) {
@@ -1241,6 +1333,11 @@
         updateAndAnimateTransientTaskbarForTimeout();
     }
 
+    @VisibleForTesting
+    Alarm getTimeoutAlarm() {
+        return mTimeoutAlarm;
+    }
+
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarStashController:");
@@ -1343,8 +1440,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/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 6b1173a..b80aaf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.OverviewCommandHelper;
@@ -47,6 +48,7 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.Collections;
@@ -55,7 +57,7 @@
 /**
  * Base class for providing different taskbar UI
  */
-public class TaskbarUIController {
+public class TaskbarUIController implements BubbleBarController.BubbleBarLocationListener {
     public static final TaskbarUIController DEFAULT = new TaskbarUIController();
 
     // Initialized in init.
@@ -428,4 +430,26 @@
     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 32d6561..8763509 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -46,6 +46,7 @@
 
 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;
@@ -63,6 +64,7 @@
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.IconButtonView;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
@@ -87,6 +89,7 @@
     private final boolean mIsRtl;
 
     private final TaskbarActivityContext mActivityContext;
+    @Nullable private BubbleBarLocation mBubbleBarLocation = null;
 
     // Initialized in init.
     private TaskbarViewCallbacks mControllerCallbacks;
@@ -99,9 +102,12 @@
     // Only non-null when device supports having an All Apps button.
     @Nullable private final TaskbarAllAppsButtonContainer mAllAppsButtonContainer;
 
-    // Only non-null when device supports having an All Apps button.
+    // Only non-null when device supports having a Divider button.
     @Nullable private TaskbarDividerContainer mTaskbarDividerContainer;
 
+    // Only non-null when device supports having a Taskbar Overflow button.
+    @Nullable private IconButtonView mTaskbarOverflowView;
+
     /**
      * Whether the divider is between Hotseat icons and Recents,
      * instead of between All Apps button and Hotseat.
@@ -114,6 +120,8 @@
 
     private boolean mShouldTryStartAlign;
 
+    private final int mMaxNumIcons;
+
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -170,8 +178,27 @@
             mTaskbarDividerContainer = new TaskbarDividerContainer(context);
         }
 
+        if (Flags.taskbarOverflow()) {
+            mTaskbarOverflowView = (IconButtonView) LayoutInflater.from(context)
+                    .inflate(R.layout.taskbar_overflow_button, this, false);
+            mTaskbarOverflowView.setIconDrawable(
+                    resources.getDrawable(R.drawable.taskbar_overflow_icon));
+            mTaskbarOverflowView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+        }
         // TODO: Disable touch events on QSB otherwise it can crash.
         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
+
+        mMaxNumIcons = calculateMaxNumIcons();
+    }
+
+    /**
+     // @return the maximum number of 'icons' that can fit in the taskbar.
+     // TODO(368119679): Assumes that they are all the same size.
+     */
+    private int calculateMaxNumIcons() {
+        int availableWidth = mActivityContext.getDeviceProfile().widthPx
+                - (mActivityContext.getDeviceProfile().edgeMarginPx * 2);
+        return Math.floorDiv(availableWidth, mIconTouchSize);
     }
 
     @Override
@@ -197,7 +224,7 @@
 
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
-        mShouldTryStartAlign = mActivityContext.isThreeButtonNav() && dp.startAlignTaskbar;
+        mShouldTryStartAlign = mActivityContext.shouldStartAlignTaskbar();
     }
 
     @Override
@@ -262,6 +289,12 @@
         if (mTaskbarDividerContainer != null && callbacks.supportsDividerLongPress()) {
             mTaskbarDividerContainer.setUpCallbacks(callbacks);
         }
+        if (mTaskbarOverflowView != null) {
+            mTaskbarOverflowView.setOnClickListener(
+                    mControllerCallbacks.getOverflowOnClickListener());
+            mTaskbarOverflowView.setOnLongClickListener(
+                    mControllerCallbacks.getOverflowOnLongClickListener());
+        }
     }
 
     private void removeAndRecycle(View view) {
@@ -289,6 +322,9 @@
                 removeView(mTaskbarDividerContainer);
             }
         }
+        if (mTaskbarOverflowView != null) {
+            removeView(mTaskbarOverflowView);
+        }
         removeView(mQsb);
 
         // Add Hotseat icons.
@@ -376,6 +412,9 @@
         if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) {
             addView(mTaskbarDividerContainer, nextViewIndex++);
             mAddedDividerForRecents = true;
+            if (mTaskbarOverflowView != null) {
+                addView(mTaskbarOverflowView, nextViewIndex++);
+            }
         }
 
         // Add Recent/Running icons.
@@ -448,6 +487,9 @@
                 addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1);
             }
         }
+
+        updateRecentAppsToFit();
+
         if (mActivityContext.getDeviceProfile().isQsbInline) {
             addView(mQsb, mIsRtl ? getChildCount() : 0);
             // Always set QSB to invisible after re-adding.
@@ -455,6 +497,45 @@
         }
     }
 
+    /**
+     * Updates the recent apps portion of the taskbar by:
+     * - Removing overflow affordance if overflow is not needed.
+     * - Removing any recent apps that do not fit.
+     */
+    private void updateRecentAppsToFit() {
+        if (!Flags.taskbarOverflow()) {
+            return;
+        }
+        int indexOfFirstRecentApp = -1;
+        int size = getChildCount();
+        boolean removeOverflowView = true;
+
+        for (int i = 0; i < size; ++i) {
+            if (getChildAt(i).getTag() instanceof GroupTask) {
+                indexOfFirstRecentApp = i;
+                removeOverflowView = false;
+                break;
+            }
+        }
+
+        if (indexOfFirstRecentApp != -1) {
+            // We pre-maturely added the overflow icon, so we can take it out of the count.
+            int numRecentAppsToRemove = Math.max(0, getChildCount() - mMaxNumIcons + 1);
+            if (numRecentAppsToRemove <= 1) {
+                // We can fit all of the recent apps if we remove the overflow icon.
+                removeOverflowView = true;
+            } else {
+                for (int i = 0; i < numRecentAppsToRemove; ++i) {
+                    removeAndRecycle(getChildAt(indexOfFirstRecentApp));
+                }
+            }
+        }
+
+        if (removeOverflowView) {
+            removeView(mTaskbarOverflowView);
+        }
+    }
+
     /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
     public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) {
         // TODO(b/343289567): support app pairs.
@@ -494,39 +575,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
@@ -558,6 +660,7 @@
         mIconLayoutBounds.right = iconEnd;
         mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
         mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
+        int count = getChildCount();
         for (int i = count; i > 0; i--) {
             View child = getChildAt(i - 1);
             if (child == mQsb) {
@@ -676,6 +779,14 @@
     }
 
     /**
+     * Returns the taskbar overflow view in the taskbar.
+     */
+    @Nullable
+    public IconButtonView getTaskbarOverflowView() {
+        return mTaskbarOverflowView;
+    }
+
+    /**
      * Returns whether the divider is between Hotseat icons and Recents,
      * instead of between All Apps button and Hotseat.
      */
@@ -770,4 +881,19 @@
         }
         return mAllAppsButtonContainer;
     }
+
+    /**
+     * 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();
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 5ec00ac..176be1c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -28,6 +28,7 @@
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 /**
@@ -127,4 +128,33 @@
         }
         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) {
+                mControllers.keyboardQuickSwitchController.openQuickSwitchView(
+                        mControllers.taskbarViewController.getTaskIdsForPinnedApps());
+            }
+        };
+    }
+
+    /** Returns on long click listener for the taskbar overflow view. */
+    public View.OnLongClickListener getOverflowOnLongClickListener() {
+        return new View.OnLongClickListener() {
+            @Override
+            public boolean onLongClick(View v) {
+                mControllers.keyboardQuickSwitchController.openQuickSwitchView(
+                        mControllers.taskbarViewController.getTaskIdsForPinnedApps());
+                return true;
+            }
+        };
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index aa3e6bf..b495e7d 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,11 +30,13 @@
 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;
-import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -66,6 +69,7 @@
 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;
@@ -74,15 +78,20 @@
 import com.android.launcher3.util.MultiValueAlpha;
 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";
 
@@ -122,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;
@@ -143,6 +156,12 @@
     // Initialized in init.
     private TaskbarControllers mControllers;
 
+    private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener =
+            (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                updateTaskbarIconTranslationXForPinning();
+                mControllers.navbarButtonsViewController.onTaskbarLayoutChanged();
+            };
+
     // 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;
@@ -206,7 +225,8 @@
         mModelCallbacks.init(controllers);
         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();
@@ -226,6 +246,54 @@
         }
     }
 
+    /** 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.
      */
@@ -459,6 +527,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.
      */
@@ -553,6 +632,24 @@
         }
     }
 
+    /**
+     * @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,
             Set<Integer> runningTaskIds,
@@ -594,8 +691,7 @@
      * @param interpolator The interpolator to use for all animations.
      */
     public void addRevealAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration,
-            Interpolator interpolator, boolean dispatchOnAnimationStart,
-            boolean isHomeToAppAnimation) {
+            Interpolator interpolator, boolean dispatchOnAnimationStart) {
         AnimatorSet reveal = new AnimatorSet();
 
         Rect stashedBounds = new Rect();
@@ -644,21 +740,8 @@
                 reveal.play(ObjectAnimator.ofFloat(mtd.getTranslationX(INDEX_TASKBAR_REVEAL_ANIM),
                         MULTI_PROPERTY_VALUE, transX)
                         .setDuration(duration));
-
-                if (enableScalingRevealHomeAnimation()) {
-                    // Delay y-translation by 1 frame to keep icons within the bounds of the bg.
-                    int delay = isHomeToAppAnimation ? getSingleFrameMs(mActivity) : 0;
-                    ObjectAnimator yAnimator =
-                            ObjectAnimator.ofFloat(mtd.getTranslationY(INDEX_TASKBAR_REVEAL_ANIM),
-                                            MULTI_PROPERTY_VALUE, transY)
-                                    .setDuration(Math.max(0, duration - delay));
-                    yAnimator.setStartDelay(delay);
-                    reveal.play(yAnimator);
-                } else {
-                    reveal.play(
-                            ObjectAnimator.ofFloat(mtd.getTranslationY(INDEX_TASKBAR_REVEAL_ANIM),
-                                    MULTI_PROPERTY_VALUE, transY));
-                }
+                reveal.play(ObjectAnimator.ofFloat(mtd.getTranslationY(INDEX_TASKBAR_REVEAL_ANIM),
+                        MULTI_PROPERTY_VALUE, transY));
                 as.addListener(forEndCallback(() ->
                         mtd.setTranslation(INDEX_TASKBAR_REVEAL_ANIM, 0, 0)));
             } else {
@@ -750,10 +833,19 @@
                 : mPersistentTaskbarDp.taskbarBottomMargin;
 
         int firstRecentTaskIndex = -1;
+        int hotseatNavBarTranslationX = 0;
+        if (mCurrentBubbleBarLocation != null
+                && taskbarDp.shouldAdjustHotseatOnBubblesLocationUpdate(mActivity)) {
+            boolean isBubblesOnLeft = mCurrentBubbleBarLocation.isOnLeft(
+                    mTaskbarView.isLayoutRtl());
+            hotseatNavBarTranslationX = taskbarDp
+                    .getHotseatTranslationXForBubbleBar(/* isNavbarOnRight = */ isBubblesOnLeft);
+        }
         for (int i = 0; i < mTaskbarView.getChildCount(); i++) {
             View child = mTaskbarView.getChildAt(i);
             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;
@@ -762,9 +854,10 @@
                 // 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())
+            } else if ((isAllAppsButton && !FeatureFlags.enableAllAppsButtonInHotseat())
                     || (isTaskbarDividerView && enableTaskbarPinning())
-                    || (isRecentTask && !isRecentsInHotseat)) {
+                    || (isRecentTask && !isRecentsInHotseat)
+                    || isTaskbarOverflowView) {
                 if (!isToHome
                         && mIsHotseatIconOnTopWhenAligned
                         && mIsStashed) {
@@ -783,16 +876,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)
@@ -801,8 +898,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);
@@ -849,6 +946,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();
@@ -1017,4 +1115,12 @@
     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/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 f5ac66f..5830095 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -39,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;
@@ -180,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);
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/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index d70a317..51e09ab 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -118,6 +118,7 @@
     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;
@@ -176,6 +177,7 @@
 
     /** Initializes controllers. */
     public void init(BubbleControllers bubbleControllers,
+            BubbleBarLocationListener bubbleBarLocationListener,
             ImeVisibilityChecker imeVisibilityChecker) {
         mImeVisibilityChecker = imeVisibilityChecker;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
@@ -183,6 +185,7 @@
         mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
         mBubblePinController = bubbleControllers.bubblePinController;
         mBubbleCreator = bubbleControllers.bubbleCreator;
+        mBubbleBarLocationListener = bubbleBarLocationListener;
 
         bubbleControllers.runAfterInit(() -> {
             mBubbleBarViewController.setHiddenForBubbles(
@@ -193,7 +196,8 @@
             mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
                     key -> setSelectedBubbleInternal(mBubbles.get(key)));
             mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged);
-
+            mBubbleBarLocationListener.onBubbleBarLocationUpdated(
+                    mBubbleBarViewController.getBubbleBarLocation());
             if (sBubbleBarEnabled) {
                 mSystemUiProxy.setBubblesListener(this);
             }
@@ -488,12 +492,16 @@
     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. */
@@ -518,4 +526,14 @@
         /** 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/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 9c34307..a34fab2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
@@ -27,6 +27,7 @@
 import android.widget.FrameLayout
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.R
+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
@@ -42,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)
@@ -86,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..bc562a6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.R
+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.StartState.COLLAPSED
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.StartState.EXPANDED
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.StartState.STASHED
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.StartState.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 expandThreshold: Int
+    private val maxOverscroll: Int
+    private val stashThreshold: Int
+
+    private var swipeState: SwipeState = SwipeState()
+
+    constructor(tac: TaskbarActivityContext) : this(tac, DefaultDimensionProvider(tac))
+
+    @VisibleForTesting
+    constructor(context: Context, dimensionProvider: DimensionProvider) {
+        this.context = context
+        unstashThreshold = dimensionProvider.unstashThreshold
+        expandThreshold = dimensionProvider.expandThreshold
+        maxOverscroll = dimensionProvider.maxOverscroll
+        stashThreshold = dimensionProvider.stashThreshold
+    }
+
+    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)
+    }
+
+    /** Update swipe distance to [dy] */
+    fun swipeTo(dy: Float) {
+        if (!canHandleSwipe(dy)) {
+            return
+        }
+        animatedSwipeTranslation.updateValue(dy)
+
+        val prevState = swipeState
+        // We can pass unstash threshold once per gesture, keep it true if it happened once
+        val passedUnstashThreshold = isUnstash(dy) || prevState.passedUnstashThreshold
+        // Expand happens at the end of the gesture, always keep the current value
+        val passedExpandThreshold = isExpand(dy)
+        // Stash happens at the end of the gesture, always keep the current value
+        val passedStashThreshold = isStash(dy)
+
+        if (
+            passedUnstashThreshold != prevState.passedUnstashThreshold ||
+                passedExpandThreshold != prevState.passedExpandThreshold ||
+                passedStashThreshold != prevState.passedStashThreshold
+        ) {
+            swipeState =
+                swipeState.copy(
+                    passedUnstashThreshold = passedUnstashThreshold,
+                    passedExpandThreshold = passedExpandThreshold,
+                    passedStashThreshold = passedStashThreshold,
+                )
+        }
+
+        if (
+            swipeState.startState == STASHED &&
+                swipeState.passedUnstashThreshold &&
+                !prevState.passedUnstashThreshold
+        ) {
+            bubbleStashController.showBubbleBar(expandBubbles = false)
+        }
+    }
+
+    /** Finish tracking swipe gesture. Animate views back to resting state */
+    fun finish() {
+        when {
+            swipeState.passedExpandThreshold &&
+                swipeState.startState in setOf(STASHED, COLLAPSED) -> {
+                bubbleStashController.showBubbleBar(expandBubbles = true)
+            }
+            swipeState.passedStashThreshold && swipeState.startState == COLLAPSED -> {
+                bubbleStashController.stashBubbleBar()
+            }
+        }
+        if (animatedSwipeTranslation.value == 0f) {
+            reset()
+        } else {
+            springToRest()
+        }
+    }
+
+    /** Returns `true` if we are tracking a swipe gesture */
+    fun isSwipeGesture(): Boolean {
+        return swipeState.passedUnstashThreshold ||
+            swipeState.passedExpandThreshold ||
+            swipeState.passedStashThreshold
+    }
+
+    private fun canHandleSwipe(dy: Float): Boolean {
+        return when (swipeState.startState) {
+            STASHED -> dy < 0 // stashed bar only handles swipe up
+            COLLAPSED -> true // collapsed bar can be swiped in either direction
+            UNKNOWN,
+            EXPANDED -> false // expanded bar can't be swiped
+        }
+    }
+
+    private fun isUnstash(dy: Float): Boolean {
+        return dy < -unstashThreshold
+    }
+
+    private fun isExpand(dy: Float): Boolean {
+        return dy < -expandThreshold
+    }
+
+    private fun isStash(dy: Float): Boolean {
+        return dy > stashThreshold
+    }
+
+    private fun reset() {
+        springAnimation?.let {
+            if (it.isRunning) {
+                it.removeAllListeners()
+                it.cancel()
+                animatedSwipeTranslation.updateValue(0f)
+            }
+        }
+        springAnimation = null
+        swipeState = SwipeState()
+    }
+
+    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: StartState = UNKNOWN,
+        val passedUnstashThreshold: Boolean = false,
+        val passedExpandThreshold: Boolean = false,
+        val passedStashThreshold: Boolean = false,
+    )
+
+    internal enum class StartState {
+        UNKNOWN,
+        STASHED,
+        COLLAPSED,
+        EXPANDED,
+    }
+
+    /** Allows overriding the dimension provider for testing */
+    @VisibleForTesting
+    interface DimensionProvider {
+        val unstashThreshold: Int
+        val expandThreshold: Int
+        val maxOverscroll: Int
+        val stashThreshold: Int
+    }
+
+    private class DefaultDimensionProvider(taskbarActivityContext: TaskbarActivityContext) :
+        DimensionProvider {
+        override val unstashThreshold: Int
+        override val expandThreshold: Int
+        override val maxOverscroll: Int
+        override val stashThreshold: Int
+
+        init {
+            val resources = taskbarActivityContext.resources
+            unstashThreshold =
+                TaskbarThresholdUtils.getFromNavThreshold(
+                    resources,
+                    taskbarActivityContext.deviceProfile,
+                )
+            // TODO(325673340): review threshold with ux
+            expandThreshold =
+                TaskbarThresholdUtils.getAppWindowThreshold(
+                    resources,
+                    taskbarActivityContext.deviceProfile,
+                )
+            maxOverscroll = taskbarActivityContext.deviceProfile.heightPx - unstashThreshold
+            stashThreshold = resources.getDimensionPixelSize(R.dimen.taskbar_to_nav_threshold)
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 1dbf445..7fed381 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -15,13 +15,9 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
-import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
@@ -42,10 +38,9 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
-import androidx.dynamicanimation.animation.SpringForce;
-
 import com.android.launcher3.R;
-import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.taskbar.BarsLocationAnimatorHelper;
 import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
 import com.android.launcher3.util.DisplayController;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -83,8 +78,9 @@
  */
 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;
@@ -93,18 +89,6 @@
     private static final int WIDTH_ANIMATION_DURATION_MS = 200;
     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.
@@ -187,6 +171,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;
@@ -335,6 +322,16 @@
     }
 
     /**
+     * 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());
+        }
+    }
+
+    /**
      * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders.
      *
      * @param newIconSize         new icon size
@@ -565,77 +562,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}.
@@ -676,7 +626,7 @@
         }
         setAlphaDuringBubbleDrag(1f);
         setTranslationX(0f);
-        if (getBubbleChildCount() > 0) {
+        if (mIsBarExpanded && getBubbleChildCount() > 0) {
             setAlpha(1f);
         }
     }
@@ -799,6 +749,9 @@
             removeView(removedBubble);
             int index = addingOverflow ? getChildCount() : 0;
             addView(addedBubble, index, lp);
+            if (onEndRunnable != null) {
+                onEndRunnable.run();
+            }
             return;
         }
         int index = addingOverflow ? getChildCount() : 0;
@@ -994,10 +947,7 @@
         final float expandedWidth = expandedWidth();
         final float collapsedWidth = collapsedWidth();
         int childCount = 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 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);
@@ -1122,15 +1072,25 @@
                 translationX = 0f;
             }
         } else {
-            if (bubbleIndex == 1 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
-                translationX = mIconOverlapAmount;
-            } else {
+            // 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 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();
+    }
+
     /**
      * Reorders the views to match the provided list.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 570b1b9..025c038 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -18,11 +18,8 @@
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
-import static com.android.launcher3.taskbar.bubbles.BubbleView.STASH_TRANSLATION_Y;
-
 import android.animation.Animator;
 import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -97,6 +94,8 @@
             this::updateBackgroundScaleY);
     private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
             this::updateTranslationY);
+    private final AnimatedFloat mBubbleOffsetY = new AnimatedFloat(
+            this::updateBubbleOffsetY);
 
     // Modified when swipe up is happening on the bubble bar or task bar.
     private float mBubbleBarSwipeUpTranslationY;
@@ -299,6 +298,10 @@
         return mBubbleBarTranslationY;
     }
 
+    public AnimatedFloat getBubbleOffsetY() {
+        return mBubbleOffsetY;
+    }
+
     public float getBubbleBarCollapsedWidth() {
         return mBarView.collapsedWidth();
     }
@@ -437,7 +440,7 @@
             if (hidden) {
                 mBarView.setAlpha(0);
                 mBarView.setExpanded(false);
-                updatePersistentTaskbar(/* isBubbleBarExpanded = */ false);
+                adjustTaskbarAndHotseatToBubbleBarState(/* isBubbleBarExpanded = */ false);
             }
             mActivity.bubbleBarVisibilityChanged(!hidden);
         }
@@ -576,6 +579,10 @@
         mBarView.setBubbleAlpha(alpha);
     }
 
+    private void updateBubbleOffsetY(float transY) {
+        mBarView.setBubbleOffsetY(transY);
+    }
+
     private void updateBackgroundAlpha(float alpha) {
         mBarView.setBackgroundAlpha(alpha);
     }
@@ -728,7 +735,7 @@
     public void setExpanded(boolean isExpanded) {
         if (isExpanded != mBarView.isExpanded()) {
             mBarView.setExpanded(isExpanded);
-            updatePersistentTaskbar(isExpanded);
+            adjustTaskbarAndHotseatToBubbleBarState(isExpanded);
             if (!isExpanded) {
                 mSystemUiProxy.collapseBubbles();
             } else {
@@ -739,13 +746,25 @@
         }
     }
 
-    private void updatePersistentTaskbar(boolean isBubbleBarExpanded) {
-        if (mBubbleStashController.isTransientTaskBar()) return;
-        boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
-        mTaskbarViewPropertiesProvider
-                .getIconsAlpha()
-                .animateToValue(hideTaskbar ? 0 : 1)
-                .start();
+    /**
+     * Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in
+     * app or overview. Set the hotseat stashed state if on launcher home screen. If not on launcher
+     * home screen and hotseat is stashed immediately un-stashes the hotseat.
+     */
+    private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) {
+        if (mBubbleStashController.isBubblesShowingOnHome()) {
+            mTaskbarStashController.stashHotseat(isBubbleBarExpanded);
+        } else if (!mBubbleStashController.isTransientTaskBar()) {
+            boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
+            mTaskbarViewPropertiesProvider
+                    .getIconsAlpha()
+                    .animateToValue(hideTaskbar ? 0 : 1)
+                    .start();
+        }
+        if (!mBubbleStashController.isBubblesShowingOnHome()
+                && mTaskbarStashController.isHiddenForBubbles()) {
+            mTaskbarStashController.unStashHotseatInstantly();
+        }
     }
 
     /** Return {@code true} if expanded bubble bar would intersect the taskbar. */
@@ -864,6 +883,11 @@
         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
      *
@@ -874,14 +898,9 @@
         mBubbleStashController.getHandleBounds(stashedHandleBounds);
         int childCount = mBarView.getChildCount();
         float newChildWidth = (float) stashedHandleBounds.width() / childCount;
-        float stashTranslationY = -mBubbleStashController.getBubbleBarTranslationY();
         AnimatorSet animatorSet = new AnimatorSet();
         for (int i = 0; i < childCount; i++) {
             BubbleView child = (BubbleView) mBarView.getChildAt(i);
-            final float startTransY = isStashed ? 0f : stashTranslationY;
-            final float endTransY = isStashed ? stashTranslationY : 0f;
-            animatorSet.play(
-                    ObjectAnimator.ofFloat(child, STASH_TRANSLATION_Y, startTransY, endTransY));
             animatorSet.play(
                     createRevealAnimForBubble(child, isStashed, stashedHandleBounds,
                             newChildWidth));
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index e00916a..b5d94bd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -22,6 +22,7 @@
 
 import com.android.launcher3.taskbar.TaskbarControllers;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleBarLocationOnDemandListener;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.RunnableList;
@@ -40,6 +41,7 @@
     public final BubbleDismissController bubbleDismissController;
     public final BubbleBarPinController bubbleBarPinController;
     public final BubblePinController bubblePinController;
+    public final Optional<BubbleBarSwipeController> bubbleBarSwipeController;
     public final BubbleCreator bubbleCreator;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
@@ -58,6 +60,7 @@
             BubbleDismissController bubbleDismissController,
             BubbleBarPinController bubbleBarPinController,
             BubblePinController bubblePinController,
+            Optional<BubbleBarSwipeController> bubbleBarSwipeController,
             BubbleCreator bubbleCreator) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
@@ -67,6 +70,7 @@
         this.bubbleDismissController = bubbleDismissController;
         this.bubbleBarPinController = bubbleBarPinController;
         this.bubblePinController = bubblePinController;
+        this.bubbleBarSwipeController = bubbleBarSwipeController;
         this.bubbleCreator = bubbleCreator;
     }
 
@@ -76,7 +80,14 @@
      * in constructors for now, as some controllers may still be waiting for init().
      */
     public void init(TaskbarControllers taskbarControllers) {
+        BubbleBarLocationCompositeListener bubbleBarLocationListeners =
+                new BubbleBarLocationCompositeListener(
+                        taskbarControllers.navbarButtonsViewController,
+                        taskbarControllers.taskbarViewController,
+                        new BubbleBarLocationOnDemandListener(() -> taskbarControllers.uiController)
+                );
         bubbleBarController.init(this,
+                bubbleBarLocationListeners,
                 taskbarControllers.navbarButtonsViewController::isImeVisible);
         bubbleStashedHandleViewController.ifPresent(
                 controller -> controller.init(/* bubbleControllers = */ this));
@@ -102,8 +113,9 @@
                 });
         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();
     }
@@ -124,6 +136,7 @@
     public void 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
index 12b1487..340a120 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
@@ -44,8 +44,6 @@
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
-import androidx.appcompat.content.res.AppCompatResources;
-
 import com.android.internal.graphics.ColorUtils;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
@@ -196,8 +194,7 @@
     }
 
     private Bitmap createOverflowBitmap() {
-        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
-                R.drawable.bubble_ic_overflow_button);
+        Drawable iconDrawable = mContext.getDrawable(R.drawable.bubble_ic_overflow_button);
 
         final TypedArray ta = mContext.obtainStyledAttributes(
                 new int[]{
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index cc6b49a..561df5c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -27,7 +27,6 @@
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.FloatProperty;
 import android.view.LayoutInflater;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
@@ -50,27 +49,12 @@
 
     public static final int DEFAULT_PATH_SIZE = 100;
 
-    public static FloatProperty<BubbleView> STASH_TRANSLATION_Y = new FloatProperty<>(
-            "stashTranslationY") {
-        @Override
-        public void setValue(BubbleView bubbleView, float transY) {
-            bubbleView.setStashTranslationY(transY);
-        }
-
-        @Override
-        public Float get(BubbleView bubbleView) {
-            return bubbleView.mStashTranslationY;
-        }
-    };
-
     private final ImageView mBubbleIcon;
     private final ImageView mAppIcon;
     private int mBubbleSize;
 
     private float mDragTranslationX;
     private float mOffsetX;
-    private float mTranslationY;
-    private float mStashTranslationY;
 
     private DotRenderer mDotRenderer;
     private DotRenderer.DrawParams mDrawParams;
@@ -177,24 +161,6 @@
         setTranslationX(mDragTranslationX + mOffsetX);
     }
 
-    /**
-     * Set translation in y direction during stash and unstash from handle
-     */
-    public void setStashTranslationY(float translationY) {
-        mStashTranslationY = translationY;
-        applyTranslationY();
-    }
-
-    @Override
-    public void setTranslationY(float translationY) {
-        mTranslationY = translationY;
-        applyTranslationY();
-    }
-
-    private void applyTranslationY() {
-        super.setTranslationY(mTranslationY + mStashTranslationY);
-    }
-
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
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..49760ff
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.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.taskbar.bubbles.flyout
+
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.core.animation.ValueAnimator
+import com.android.launcher3.R
+
+/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
+class BubbleBarFlyoutController(
+    private val container: FrameLayout,
+    private val positioner: BubbleBarFlyoutPositioner,
+) {
+
+    private var flyout: BubbleBarFlyoutView? = null
+    private val horizontalMargin =
+        container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
+
+    fun setUpFlyout(message: BubbleBarFlyoutMessage) {
+        flyout?.let(container::removeView)
+        val flyout = BubbleBarFlyoutView(container.context, positioner)
+
+        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)
+
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.addUpdateListener { _ ->
+            flyout.updateExpansionProgress(animator.animatedValue as Float)
+        }
+        flyout.showFromCollapsed(message) { animator.start() }
+        this.flyout = flyout
+    }
+
+    fun hideFlyout() {
+        val flyout = this.flyout ?: return
+        container.removeView(flyout)
+        this.flyout = 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..7298297
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.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.taskbar.bubbles.flyout
+
+import android.graphics.drawable.Drawable
+
+data class BubbleBarFlyoutMessage(
+    val senderAvatar: Drawable?,
+    val senderName: CharSequence,
+    val message: CharSequence,
+    val isGroupChat: Boolean,
+)
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..b8c1c7f
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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
+}
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..6723f57
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.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) :
+    ConstraintLayout(context) {
+
+    private companion object {
+        // the minimum progress of the expansion animation before the triangle is made visible.
+        const val MIN_EXPANSION_PROGRESS_FOR_TRIANGLE = 0.1f
+        // 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 sender: TextView by
+        lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_name) }
+
+    private val avatar: ImageView by
+        lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_avatar) }
+
+    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)
+        }
+
+    /** 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 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)
+
+    init {
+        LayoutInflater.from(context).inflate(R.layout.bubblebar_flyout, this, true)
+
+        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 =
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_elevation).toFloat()
+
+        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)
+    }
+
+    /** Sets the data for the flyout and starts playing the expand animation. */
+    fun showFromCollapsed(flyoutMessage: BubbleBarFlyoutMessage, expandAnimation: () -> Unit) {
+        avatar.alpha = 0f
+        sender.alpha = 0f
+        message.alpha = 0f
+        setData(flyoutMessage)
+        val txToCollapsedPosition =
+            if (positioner.isOnLeft) {
+                positioner.distanceToCollapsedPosition.x
+            } else {
+                -positioner.distanceToCollapsedPosition.x
+            }
+        val tyToCollapsedPosition =
+            positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
+        translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
+
+        collapsedSize = positioner.collapsedSize
+        collapsedCornerRadius = collapsedSize / 2
+        collapsedColor = positioner.collapsedColor
+
+        // post the request to start the expand animation to the looper so the view can measure
+        // itself
+        post(expandAnimation)
+    }
+
+    private fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
+        // the avatar is only displayed in group chat messages
+        if (flyoutMessage.senderAvatar != null && flyoutMessage.isGroupChat) {
+            avatar.visibility = VISIBLE
+            avatar.setImageDrawable(flyoutMessage.senderAvatar)
+        } else {
+            avatar.visibility = GONE
+        }
+
+        val minTextViewWidth: Int
+        val maxTextViewWidth: Int
+        if (avatar.visibility == VISIBLE) {
+            minTextViewWidth = minFlyoutWidth - avatar.width - flyoutPadding * 2
+            maxTextViewWidth = maxFlyoutWidth - avatar.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.senderName.isEmpty()) {
+            sender.visibility = GONE
+        } else {
+            sender.minWidth = minTextViewWidth
+            sender.maxWidth = maxTextViewWidth
+            sender.text = flyoutMessage.senderName
+            sender.visibility = VISIBLE
+        }
+
+        message.minWidth = minTextViewWidth
+        message.maxWidth = maxTextViewWidth
+        message.text = flyoutMessage.message
+    }
+
+    /** Updates the flyout view with the progress of the animation. */
+    fun updateExpansionProgress(fraction: Float) {
+        expansionProgress = fraction
+
+        updateTranslationForAnimation(message)
+        updateTranslationForAnimation(sender)
+        updateTranslationForAnimation(avatar)
+
+        // 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)
+        sender.alpha = alpha
+        message.alpha = alpha
+        avatar.alpha = alpha
+
+        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 >= MIN_EXPANSION_PROGRESS_FOR_TRIANGLE) {
+            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, starting
+        // with half the size. this has the effect of the triangle scaling.
+        val triangleY = height - triangleHeight - 0.5f * triangleHeight * (1 - expansionProgress)
+        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 >= MIN_EXPANSION_PROGRESS_FOR_TRIANGLE) {
+            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)
+        sender.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/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/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 5d1e890..74e3c00 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -32,8 +32,8 @@
 import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.anim.SpringAnimationBuilder
 import com.android.launcher3.taskbar.TaskbarInsetsController
-import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_DURATION
 import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY
+import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
@@ -47,7 +47,7 @@
 
 class TransientBubbleStashController(
     private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
-    private val context: Context
+    private val context: Context,
 ) : BubbleStashController {
 
     private lateinit var bubbleBarViewController: BubbleBarViewController
@@ -70,6 +70,7 @@
     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 =
@@ -143,13 +144,14 @@
         taskbarInsetsController: TaskbarInsetsController,
         bubbleBarViewController: BubbleBarViewController,
         bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
-        controllersAfterInitAction: ControllersAfterInitAction
+        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
@@ -170,7 +172,7 @@
                 bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
                 bubbleBarAlpha.animateToValue(1f),
                 bubbleBarBubbleAlpha.animateToValue(1f),
-                bubbleBarBackgroundAlpha.animateToValue(1f)
+                bubbleBarBackgroundAlpha.animateToValue(1f),
             )
         } else {
             isStashed = true
@@ -301,23 +303,25 @@
     private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet {
         val animatorSet = AnimatorSet()
 
-        val alphaDuration = if (isStashed) duration else TASKBAR_STASH_ALPHA_DURATION
-        val alphaDelay = if (isStashed) TASKBAR_STASH_ALPHA_START_DELAY else 0L
         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
             }
         )
 
-        val iconAlphaTarget = if (isStashed) 0f else 1f
         animatorSet.play(
-            bubbleBarBubbleAlpha.animateToValue(iconAlphaTarget).apply {
-                this.duration = TASKBAR_STASH_ALPHA_DURATION
-                this.startDelay = TASKBAR_STASH_ALPHA_START_DELAY
-                this.interpolator = LINEAR
-            }
+            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(
@@ -334,6 +338,16 @@
             }
         )
 
+        // 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
@@ -359,11 +373,15 @@
         )
 
         animatorSet.doOnStart {
-            if (!isStashed) {
-                bubbleBarBackgroundAlpha.updateValue(0f)
-                bubbleBarBubbleAlpha.updateValue(0f)
-                bubbleBarAlpha.value = 1f
-            }
+            // 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
@@ -373,6 +391,9 @@
                     // reset bubble view alpha
                     bubbleBarBubbleAlpha.updateValue(1f)
                     bubbleBarBackgroundAlpha.updateValue(1f)
+                    // reset stash translation
+                    translationYDuringStash.updateValue(0f)
+                    bubbleBarBubbleTranslationY.updateValue(0f)
                     bubbleBarViewController.isExpanded = false
                 }
                 taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -382,14 +403,29 @@
     }
 
     private fun createBackgroundAlphaAnimator(isStashed: Boolean): AnimatorSet {
-        val stashHandleAlphaTarget = if (isStashed) 1f else 0f
-        val barAlphaTarget = if (isStashed) 0f else 1f
         return AnimatorSet().apply {
-            play(bubbleBarBackgroundAlpha.animateToValue(barAlphaTarget))
-            play(stashHandleViewAlpha?.animateToValue(stashHandleAlphaTarget))
+            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
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
index 726800c..e6c0b2f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -30,7 +30,6 @@
 import androidx.core.view.setPadding
 import com.android.launcher3.R
 import com.android.launcher3.Utilities.dpToPx
-import com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.TaskbarViewCallbacks
@@ -43,11 +42,8 @@
 /** Taskbar all apps button container for customizable taskbar. */
 class TaskbarAllAppsButtonContainer
 @JvmOverloads
-constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0,
-) : IconButtonView(context, attrs), TaskbarContainer {
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    IconButtonView(context, attrs), TaskbarContainer {
 
     private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
     private var allAppsTouchTriggered = false
@@ -101,23 +97,16 @@
     private fun getAllAppsButton(isTransientTaskbar: Boolean): Int {
         val shouldSelectTransientIcon =
             isTransientTaskbar || (enableTaskbarPinning() && !activityContext.isThreeButtonNav)
-        return if (ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
-            if (shouldSelectTransientIcon) R.drawable.ic_transient_taskbar_all_apps_search_button
-            else R.drawable.ic_taskbar_all_apps_search_button
-        } else {
-            if (shouldSelectTransientIcon) R.drawable.ic_transient_taskbar_all_apps_button
-            else R.drawable.ic_taskbar_all_apps_button
-        }
+        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 if (ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
-            R.dimen.taskbar_all_apps_search_button_translation_x_offset
         } else {
-            R.dimen.taskbar_all_apps_button_translation_x_offset
+            R.dimen.taskbar_all_apps_search_button_translation_x_offset
         }
     }
 
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/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
index 5b07926..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,11 +29,9 @@
 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;
-import com.android.launcher3.util.StableViewInfo;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
 /** Provides a Quickstep specific animation when launching an activity from an app widget. */
@@ -67,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 info) {
-            launchCookie = StableViewInfo.toLaunchCookie(info);
-            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 {
@@ -82,7 +73,7 @@
                         .registerRemoteAnimationForNextActivityStart(
                                 pendingIntent.getCreatorPackage(),
                                 activityOptions.options.getRemoteAnimationAdapter(),
-                                launchCookie);
+                                activityOptions.options.getLaunchCookie());
             } catch (RemoteException e) {
                 // Do nothing.
             }
@@ -93,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 b5b8909..bc0ace2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -19,6 +19,7 @@
 import static android.os.Trace.TRACE_TAG_APP;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
+import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
@@ -36,6 +37,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
+import static com.android.launcher3.Utilities.isRtl;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
@@ -59,13 +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.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.Flags.enableBubbleAnything;
+import static com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -172,7 +173,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;
@@ -198,10 +199,9 @@
 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;
 import java.util.ArrayList;
@@ -214,6 +214,8 @@
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import kotlin.Unit;
+
 public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
         SystemShortcut.BubbleActivityStarter {
     private static final boolean TRACE_LAYOUTS =
@@ -227,7 +229,6 @@
     private FixedContainerItems mAllAppsPredictions;
     private HotseatPredictionController mHotseatPredictionController;
     private DepthController mDepthController;
-    private @Nullable DesktopVisibilityController mDesktopVisibilityController;
     private QuickstepTransitionManager mAppTransitionManager;
 
     private OverviewActionsView<?> mActionsView;
@@ -240,6 +241,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
@@ -272,7 +274,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());
@@ -301,8 +303,6 @@
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
         mDepthController = new DepthController(this);
         if (DesktopModeStatus.canEnterDesktopMode(this)) {
-            mDesktopVisibilityController = new DesktopVisibilityController(this);
-            mDesktopVisibilityController.registerSystemUiListener();
             mSplitSelectStateController.initSplitFromDesktopController(this,
                     overviewComponentObserver);
         }
@@ -466,7 +466,7 @@
         if (Flags.enablePrivateSpace()) {
             shortcuts.add(UNINSTALL_APP);
         }
-        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+        if (enableBubbleAnything()) {
             shortcuts.add(BUBBLE_SHORTCUT);
         }
         return shortcuts.stream();
@@ -554,10 +554,6 @@
             mLauncherUnfoldAnimationController.onDestroy();
         }
 
-        if (mDesktopVisibilityController != null) {
-            mDesktopVisibilityController.unregisterSystemUiListener();
-        }
-
         if (mSplitSelectStateController != null) {
             mSplitSelectStateController.onDestroy();
         }
@@ -602,11 +598,8 @@
                     TaskView taskToLaunch = currentPageTask;
                     if (currentPageTask == null) {
                         taskToLaunch = fallbackTask;
-                        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                                "Quick switch from home fallback case: The TaskView at index ")
-                                        .append(rv.getCurrentPage())
-                                        .append(" is missing."),
-                                QUICK_SWITCH_FROM_HOME_FALLBACK);
+                        ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFallback(
+                                rv.getCurrentPage());
                     }
                     taskToLaunch.launchWithoutAnimation(success -> {
                         if (!success) {
@@ -617,11 +610,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;
@@ -688,10 +677,6 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        // Back dispatcher is registered in {@link BaseActivity#onCreate}. For predictive back to
-        // work, we must opt-in BEFORE registering back dispatcher. So we need to call
-        // setEnableOnBackInvokedCallback() before super.onCreate()
-        getApplicationInfo().setEnableOnBackInvokedCallback(true);
         super.onCreate(savedInstanceState);
         if (savedInstanceState != null) {
             mPendingSplitSelectInfo = ObjectWrapper.unwrap(
@@ -699,9 +684,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);
@@ -1011,10 +994,11 @@
 
     @Override
     public void setResumed() {
-        if (!WALLPAPER_ACTIVITY.isEnabled(this)
-                && 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;
@@ -1113,6 +1097,29 @@
         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
+                    .getHotseatTranslationXForBubbleBar(/* isNavbarOnRight = */ isBubblesOnLeft);
+        }
+        if (isBubbleBarEnabled() && hasBubbles()) {
+            // TODO(368379159) : create a class to reuse computation logic
+            float adjustedBorderSpace =
+                    mDeviceProfile.getHotseatAdjustedBorderSpaceForBubbleBar(this);
+            if (Float.compare(adjustedBorderSpace, 0f) != 0) {
+                float borderSpaceDelta = adjustedBorderSpace - mDeviceProfile.hotseatBorderSpace;
+                translationX +=
+                        (int) (mDeviceProfile.iconSizePx + itemInfo.cellX * borderSpaceDelta);
+            }
+        }
+        return translationX;
+    }
+
     public SplitToWorkspaceController getSplitToWorkspaceController() {
         return mSplitToWorkspaceController;
     }
@@ -1154,8 +1161,9 @@
     }
 
     @Nullable
+    @Override
     public DesktopVisibilityController getDesktopVisibilityController() {
-        return mDesktopVisibilityController;
+        return mTISBindHelper.getDesktopVisibilityController();
     }
 
     @Nullable
@@ -1190,7 +1198,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);
@@ -1308,8 +1317,9 @@
 
     @Override
     public boolean areDesktopTasksVisible() {
-        if (mDesktopVisibilityController != null) {
-            return mDesktopVisibilityController.areDesktopTasksVisible();
+        DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
+        if (desktopVisibilityController != null) {
+            return desktopVisibilityController.areDesktopTasksVisible();
         }
         return false;
     }
@@ -1350,7 +1360,7 @@
                 /* callback= */ success -> mSplitSelectStateController.resetState(),
                 /* freezeTaskList= */ false,
                 groupTask.mSplitBounds == null
-                        ? SNAP_TO_50_50
+                        ? SNAP_TO_2_50_50
                         : groupTask.mSplitBounds.snapPosition,
                 remoteTransition);
     }
@@ -1415,6 +1425,11 @@
         SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
     }
 
+    /** Sets the location of the bubble bar */
+    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+        mBubbleBarLocation = bubbleBarLocation;
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<QuickstepLauncher> {
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 1ba784b..e87ac2f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -50,9 +50,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);
@@ -88,6 +90,11 @@
     }
 
     @Override
+    public boolean detachDesktopCarousel() {
+        return true;
+    }
+
+    @Override
     protected float getDepthUnchecked(Context context) {
         if (Launcher.getLauncher(context).areDesktopTasksVisible()) {
             // Don't blur the background while desktop tasks are visible
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index b165cdd..c48ba4f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -166,6 +166,11 @@
     }
 
     @Override
+    public boolean detachDesktopCarousel() {
+        return false;
+    }
+
+    @Override
     public boolean disallowTaskbarGlobalDrag() {
         // Disable global drag in overview
         return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/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/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 0da7b2d..9164405 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -129,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(
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index b6a0ab3..b562838 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -142,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/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 978d8c4..9ad6b303 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -45,6 +45,7 @@
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
+import static com.android.quickstep.BaseContainerInterface.AnimationFactory;
 import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
 import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
@@ -55,11 +56,7 @@
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
@@ -92,6 +89,7 @@
 import android.view.animation.Interpolator;
 import android.widget.Toast;
 import android.window.PictureInPictureSurfaceTransaction;
+import android.window.flags.DesktopModeFlags;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -102,6 +100,7 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.launcher3.AbstractFloatingView;
 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.AnimationSuccessListener;
@@ -111,6 +110,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.taskbar.TaskbarThresholdUtils;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -119,11 +119,12 @@
 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.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.InputConsumerProxy;
@@ -152,12 +153,9 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.wm.shell.shared.TransactionPool;
-import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils;
 
-import kotlin.Unit;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -167,15 +165,16 @@
 import java.util.OptionalInt;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
 public abstract class AbsSwipeUpHandler<
-        RECENTS_CONTAINER extends Context & RecentsViewContainer,
-        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>,
-        STATE extends BaseState<STATE>>
-        extends SwipeUpAnimationLogic
-        implements OnApplyWindowInsetsListener, RecentsAnimationCallbacks.RecentsAnimationListener {
+        RECENTS_CONTAINER extends Context & RecentsViewContainer & StatefulContainer<STATE>,
+        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>, STATE extends BaseState<STATE>>
+        extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener,
+        RecentsAnimationCallbacks.RecentsAnimationListener {
     private static final String TAG = "AbsSwipeUpHandler";
 
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
@@ -192,7 +191,6 @@
 
     // 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 RECENTS_CONTAINER mContainer;
     protected @Nullable RECENTS_VIEW mRecentsView;
@@ -202,7 +200,7 @@
     private boolean mRecentsViewScrollLinked = false;
 
     private final Runnable mLauncherOnDestroyCallback = () -> {
-        ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED);
+        ActiveGestureProtoLogProxy.logLauncherDestroyed();
         mRecentsView = null;
         mContainer = null;
         mStateCallback.clearState(STATE_LAUNCHER_PRESENT);
@@ -293,7 +291,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
@@ -354,9 +352,12 @@
     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();
+        if (recentsWindowManager != null && Flags.enableFallbackOverviewInWindow()) {
+            recentsWindowManager.registerInitListener(this::onActivityInit);
+        }
         mActivityInitListener =
                 mContainerInterface.createActivityInitListener(this::onActivityInit);
         mInputConsumerProxy =
@@ -370,6 +371,7 @@
                     endLauncherTransitionController();
                 }, new InputProxyHandlerFactory(mContainerInterface, mGestureState));
         mTaskAnimationManager = taskAnimationManager;
+        mRecentsWindowManager = recentsWindowManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
 
@@ -533,14 +535,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();
             }
         });
@@ -827,7 +822,7 @@
             return;
         }
         initTransitionEndpoints(mContainer.getDeviceProfile());
-        mAnimationFactory.createActivityInterface(mTransitionDragLength);
+        mAnimationFactory.createContainerInterface(mTransitionDragLength);
     }
 
     /**
@@ -871,10 +866,13 @@
         }
     }
 
+    public Intent getHomeIntent() {
+        return mGestureState.getHomeIntent();
+    }
+
     public Intent getLaunchIntent() {
         return mGestureState.getOverviewIntent();
     }
-
     /**
      * Called when the value of {@link #mCurrentShift} changes
      */
@@ -983,13 +981,8 @@
 
     @Override
     public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "cancelRecentsAnimation",
-                /* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
+        ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerOnRecentsAnimationCanceled();
         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;
         mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
         // Defer clearing the controller and the targets until after we've updated the state
         mRecentsAnimationController = null;
@@ -1192,10 +1185,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. */
@@ -1211,11 +1201,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;
         }
@@ -1229,19 +1217,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.
@@ -1267,8 +1246,8 @@
                 ? mRecentsView.getCurrentPageTaskView() : null;
 
         if (DesktopModeStatus.canEnterDesktopMode(mContext)
-                && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
-                && DesktopModeFlags.QUICK_SWITCH.isEnabled(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) {
@@ -1434,8 +1413,8 @@
         };
 
         if (DesktopModeStatus.canEnterDesktopMode(mContext)
-                && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
-                && DesktopModeFlags.QUICK_SWITCH.isEnabled(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
@@ -1992,9 +1971,7 @@
      * 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();
 
@@ -2260,8 +2237,8 @@
         });
 
         if (DesktopModeStatus.canEnterDesktopMode(mContext)
-                && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
-                        && DesktopModeFlags.QUICK_SWITCH.isEnabled(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;
@@ -2295,18 +2272,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(
@@ -2315,7 +2289,7 @@
                 if (!hasTaskPreviouslyAppeared) {
                     ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
                 }
-                ActiveGestureLog.INSTANCE.addLog(nextTaskLog);
+                ActiveGestureProtoLogProxy.logStartNewTask(nextTaskLog);
                 nextTask.launchWithoutAnimation(true, success -> {
                     resultCallback.accept(success);
                     if (success) {
@@ -2393,31 +2367,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;
         }
@@ -2425,8 +2396,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;
         }
@@ -2435,12 +2406,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;
         }
@@ -2496,7 +2469,7 @@
         if (mRecentsAnimationController != null) {
             mRecentsAnimationController.finish(false /* toRecents */, onFinishComplete);
         }
-        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
+        ActiveGestureProtoLogProxy.logFinishRecentsAnimationOnTasksAppeared();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 8703843..1e755eb 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -36,7 +36,6 @@
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
 
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
@@ -62,17 +61,14 @@
 public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
         ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE> & RecentsViewContainer> extends
         BaseContainerInterface<STATE_TYPE, ACTIVITY_TYPE> {
-    private final STATE_TYPE mBackgroundState;
 
     private STATE_TYPE mTargetState;
 
-    @Nullable private Runnable mOnInitBackgroundStateUICallback = null;
-
     protected BaseActivityInterface(boolean rotationSupportedByActivity,
             STATE_TYPE overviewState, STATE_TYPE backgroundState) {
+        super(backgroundState);
         this.rotationSupportedByActivity = rotationSupportedByActivity;
         mTargetState = overviewState;
-        mBackgroundState = backgroundState;
     }
 
     /**
@@ -124,13 +120,6 @@
         return activity != null && activity.isStarted();
     }
 
-    @UiThread
-    @Nullable
-    public abstract <T extends RecentsView> T getVisibleRecentsView();
-
-    @UiThread
-    public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
-
     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
         TaskbarUIController controller = getTaskbarController();
         boolean isEventOverBubbleBarStashHandle =
@@ -163,49 +152,6 @@
         recentsView.switchToScreenshot(thumbnailDatas, runnable);
     }
 
-
-    protected void runOnInitBackgroundStateUI(Runnable callback) {
-        ACTIVITY_TYPE activity = getCreatedContainer();
-        if (activity != null && activity.getStateManager().getState() == mBackgroundState) {
-            callback.run();
-            onInitBackgroundStateUI();
-            return;
-        }
-        mOnInitBackgroundStateUICallback = callback;
-    }
-
-    private void onInitBackgroundStateUI() {
-        if (mOnInitBackgroundStateUICallback != null) {
-            mOnInitBackgroundStateUICallback.run();
-            mOnInitBackgroundStateUICallback = null;
-        }
-    }
-
-    public interface AnimationFactory {
-
-        void createActivityInterface(long transitionLength);
-
-        /**
-         * @param attached Whether to show RecentsView alongside the app window. If false, recents
-         *                 will be hidden by some property we can animate, e.g. alpha.
-         * @param animate Whether to animate recents to/from its new attached state.
-         * @param updateRunningTaskAlpha Whether to update the running task's attached alpha
-         */
-        default void setRecentsAttachedToAppWindow(
-                boolean attached, boolean animate, boolean updateRunningTaskAlpha) { }
-
-        default boolean isRecentsAttachedToAppWindow() {
-            return false;
-        }
-
-        default boolean hasRecentsEverAttachedToAppWindow() {
-            return false;
-        }
-
-        /** Called when the gesture ends and we know what state it is going towards */
-        default void setEndTarget(GestureState.GestureEndTarget endTarget) { }
-    }
-
     class DefaultAnimationFactory implements AnimationFactory {
 
         protected final ACTIVITY_TYPE mActivity;
@@ -234,7 +180,7 @@
         }
 
         @Override
-        public void createActivityInterface(long transitionLength) {
+        public void createContainerInterface(long transitionLength) {
             PendingAnimation pa = new PendingAnimation(transitionLength * 2);
             createBackgroundToOverviewAnim(mActivity, pa);
             AnimatorPlaybackController controller = pa.createPlaybackController();
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index d7378ec..14c2cc4 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -34,16 +34,19 @@
 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.fallback.window.RecentsWindowManager;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
@@ -57,13 +60,28 @@
 import java.util.function.Predicate;
 
 public abstract class BaseContainerInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
-        CONTAINER_TYPE extends RecentsViewContainer> {
+        CONTAINER_TYPE extends RecentsViewContainer & StatefulContainer<STATE_TYPE>> {
 
     public boolean rotationSupportedByActivity = false;
+    protected final STATE_TYPE mBackgroundState;
+
+    protected BaseContainerInterface(STATE_TYPE backgroundState) {
+        mBackgroundState = backgroundState;
+    }
+
+    @UiThread
+    @Nullable
+    public abstract <T extends RecentsView<?,?>> T getVisibleRecentsView();
+
+    @UiThread
+    public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
 
     @Nullable
     public abstract CONTAINER_TYPE getCreatedContainer();
 
+    @Nullable
+    protected Runnable mOnInitBackgroundStateUICallback = null;
+
     public abstract boolean isInLiveTileMode();
 
     public abstract void onAssistantVisibilityChanged(float assistantVisibility);
@@ -88,7 +106,32 @@
     @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);
 
@@ -126,9 +169,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();
     }
 
     /**
@@ -401,4 +457,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/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..ea478dd
--- /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.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+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 ActivityInitListener createActivityInitListener(
+            Predicate<Boolean> onInitListener) {
+        //todo figure out how to properly replace this
+        return new ActivityInitListener<>((activity, alreadyOnHome) ->
+                onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
+    }
+
+    @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/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 81c9d4a..2892d2c 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;
@@ -38,9 +37,11 @@
 import androidx.annotation.Nullable;
 
 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 +180,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 +191,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 +270,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;
     }
 
@@ -411,10 +411,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 +467,6 @@
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
-        mRecentsAnimationController = controller;
         mStateCallback.setState(STATE_RECENTS_ANIMATION_STARTED);
     }
 
@@ -480,10 +476,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 +514,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/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index abd71d5..85312e4 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -40,7 +40,6 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.PendingAnimation;
 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;
@@ -77,7 +76,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);
         }
     }
 
@@ -167,16 +166,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) {
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index a4ee3dd..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;
@@ -448,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);
     }
@@ -471,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
@@ -495,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();
@@ -535,7 +510,7 @@
             }
         });
         mScrimAlphaAnimator.setDuration(SCRIM_FADE_DURATION).start();
-        anim.start();
+        backAnim.start();
     }
 
     private void loadResources() {
@@ -568,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/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index d2dcd7b..dacafd4 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.views.FloatingView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
@@ -68,7 +69,7 @@
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
             boolean continuingLastGesture, InputConsumerController inputConsumer) {
         super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
-                continuingLastGesture, inputConsumer);
+                continuingLastGesture, inputConsumer, null);
     }
 
 
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
index df42efc..a9f196d 100644
--- a/quickstep/src/com/android/quickstep/MultiStateCallback.java
+++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -114,10 +115,9 @@
             if (gestureEvent == null) {
                 continue;
             }
-            if (gestureEvent.mLogEvent && gestureEvent.mTrackEvent) {
-                ActiveGestureLog.INSTANCE.addLog(gestureEvent.name(), gestureEvent);
-            } else if (gestureEvent.mLogEvent) {
-                ActiveGestureLog.INSTANCE.addLog(gestureEvent.name());
+            if (gestureEvent.mLogEvent) {
+                ActiveGestureProtoLogProxy.logDynamicString(
+                        gestureEvent.name(), gestureEvent.mTrackEvent ? gestureEvent : null);
             } else if (gestureEvent.mTrackEvent) {
                 ActiveGestureLog.INSTANCE.trackEvent(gestureEvent);
             }
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index c1f9963..a33e5c0 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -29,7 +29,6 @@
 import com.android.internal.jank.Cuj
 import com.android.launcher3.Flags.enableOverviewCommandHelperTimeout
 import com.android.launcher3.PagedView
-import com.android.launcher3.config.FeatureFlags
 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
@@ -46,8 +45,8 @@
 import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
 import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
 import com.android.quickstep.util.ActiveGestureLog
+import com.android.quickstep.util.ActiveGestureProtoLogProxy
 import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskView
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper
@@ -81,19 +80,11 @@
      */
     private var keyboardTaskFocusIndex = -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 var waitForToggleCommandComplete = false
-
-    private val activityInterface: BaseActivityInterface<*, *>
-        get() = overviewComponentObserver.activityInterface
+    private val containerInterface: BaseContainerInterface<*, *>
+        get() = overviewComponentObserver.containerInterface
 
     private val visibleRecentsView: RecentsView<*, *>?
-        get() = activityInterface.getVisibleRecentsView<RecentsView<*, *>>()
+        get() = containerInterface.getVisibleRecentsView<RecentsView<*, *>>()
 
     /**
      * Adds a command to be executed next, after all pending tasks are completed. Max commands that
@@ -175,12 +166,6 @@
      */
     @VisibleForTesting
     fun executeCommand(command: CommandInfo, onCallbackResult: () -> Unit): Boolean {
-        // This shouldn't happen if we execute 1 command per time.
-        if (waitForToggleCommandComplete && command.type == TOGGLE) {
-            Log.d(TAG, "executeCommand: $command - waiting for toggle command complete")
-            return true
-        }
-
         val recentsView = visibleRecentsView
         Log.d(TAG, "executeCommand: $command - visibleRecentsView: $recentsView")
         return if (recentsView != null) {
@@ -248,11 +233,10 @@
         recents: RecentsView<*, *>,
         taskView: TaskView?,
         command: CommandInfo,
-        onCallbackResult: () -> Unit
+        onCallbackResult: () -> Unit,
     ): Boolean {
         var callbackList: RunnableList? = null
         if (taskView != null) {
-            waitForToggleCommandComplete = true
             taskView.isEndQuickSwitchCuj = true
             callbackList = taskView.launchWithAnimation()
         }
@@ -261,28 +245,25 @@
             callbackList.add {
                 Log.d(TAG, "launching task callback: $command")
                 onCallbackResult()
-                waitForToggleCommandComplete = false
             }
             Log.d(TAG, "launching task - waiting for callback: $command")
             return false
         } else {
             recents.startHome()
-            waitForToggleCommandComplete = false
             return true
         }
     }
 
     private fun executeWhenRecentsIsNotVisible(
         command: CommandInfo,
-        onCallbackResult: () -> Unit
+        onCallbackResult: () -> Unit,
     ): Boolean {
-        val recentsViewContainer = activityInterface.getCreatedContainer() as? RecentsViewContainer
+        val recentsViewContainer = containerInterface.getCreatedContainer()
         val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
         val deviceProfile = recentsViewContainer?.getDeviceProfile()
-        val uiController = activityInterface.getTaskbarController()
+        val uiController = containerInterface.getTaskbarController()
         val allowQuickSwitch =
-            FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() &&
-                uiController != null &&
+            uiController != null &&
                 deviceProfile != null &&
                 (deviceProfile.isTablet || deviceProfile.isTwoPanels)
 
@@ -300,7 +281,7 @@
                     keyboardTaskFocusIndex = 0
                 }
             HOME -> {
-                ActiveGestureLog.INSTANCE.addLog("OverviewCommandHelper.executeCommand(HOME)")
+                ActiveGestureProtoLogProxy.logExecuteHomeCommand()
                 // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
                 // we should still call it on main thread because launcher is waiting for
                 // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
@@ -335,13 +316,13 @@
                     onCallbackResult()
                 }
             }
-        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
+        if (containerInterface.switchToRecentsIfVisible(animatorListener)) {
             Log.d(TAG, "switching to Overview state - waiting: $command")
             // If successfully switched, wait until animation finishes
             return false
         }
 
-        val activity = activityInterface.getCreatedContainer()
+        val activity = containerInterface.getCreatedContainer()
         if (activity != null) {
             InteractionJankMonitorWrapper.begin(activity.rootView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
         }
@@ -349,13 +330,13 @@
         val gestureState =
             touchInteractionService.createGestureState(
                 GestureState.DEFAULT_STATE,
-                GestureState.TrackpadGestureType.NONE
+                GestureState.TrackpadGestureType.NONE,
             )
         gestureState.isHandlingAtomicEvent = true
         val interactionHandler =
             touchInteractionService.swipeUpHandlerFactory.newHandler(
                 gestureState,
-                command.createTime
+                command.createTime,
             )
         interactionHandler.setGestureEndCallback {
             onTransitionComplete(command, interactionHandler, onCallbackResult)
@@ -366,12 +347,12 @@
             object : RecentsAnimationCallbacks.RecentsAnimationListener {
                 override fun onRecentsAnimationStart(
                     controller: RecentsAnimationController,
-                    targets: RecentsAnimationTargets
+                    targets: RecentsAnimationTargets,
                 ) {
                     Log.d(TAG, "recents animation started: $command")
                     updateRecentsViewFocus(command)
                     logShowOverviewFrom(command.type)
-                    activityInterface.runOnInitBackgroundStateUI {
+                    containerInterface.runOnInitBackgroundStateUI {
                         Log.d(TAG, "recents animation started - onInitBackgroundStateUI: $command")
                         interactionHandler.onGestureEnded(0f, PointF())
                     }
@@ -385,7 +366,7 @@
                     interactionHandler.onGestureCancelled()
                     command.removeListener(this)
 
-                    activityInterface.getCreatedContainer() ?: return
+                    containerInterface.getCreatedContainer() ?: return
                     recentsView?.onRecentsAnimationComplete()
                 }
             }
@@ -418,7 +399,7 @@
     private fun onTransitionComplete(
         command: CommandInfo,
         handler: AbsSwipeUpHandler<*, *, *>,
-        onCommandResult: () -> Unit
+        onCommandResult: () -> Unit,
     ) {
         Log.d(TAG, "switching via recents animation - onTransitionComplete: $command")
         command.removeListener(handler)
@@ -434,7 +415,7 @@
             Log.d(
                 TAG,
                 "next task not scheduled. First pending command type " +
-                    "is ${commandQueue.firstOrNull()} - command type is: $command"
+                    "is ${commandQueue.firstOrNull()} - command type is: $command",
             )
             return
         }
@@ -492,7 +473,7 @@
     }
 
     private fun logShowOverviewFrom(commandType: CommandType) {
-        val container = activityInterface.getCreatedContainer() as? RecentsViewContainer ?: return
+        val container = containerInterface.getCreatedContainer() ?: return
         val event =
             when (commandType) {
                 SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
@@ -519,7 +500,6 @@
             pw.println("    pendingCommandType=${commandQueue.first().type}")
         }
         pw.println("  keyboardTaskFocusIndex=$keyboardTaskFocusIndex")
-        pw.println("  waitForToggleCommandComplete=$waitForToggleCommandComplete")
     }
 
     @VisibleForTesting
@@ -527,7 +507,7 @@
         val type: CommandType,
         var status: CommandStatus = CommandStatus.IDLE,
         val createTime: Long = SystemClock.elapsedRealtime(),
-        private var animationCallbacks: RecentsAnimationCallbacks? = null
+        private var animationCallbacks: RecentsAnimationCallbacks? = null,
     ) {
         fun setAnimationCallbacks(recentsAnimationCallbacks: RecentsAnimationCallbacks) {
             this.animationCallbacks = recentsAnimationCallbacks
@@ -545,7 +525,7 @@
             IDLE,
             PROCESSING,
             COMPLETED,
-            CANCELED
+            CANCELED,
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index ca19480..66112c1 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,8 +151,8 @@
         // 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()) {
@@ -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.enableFallbackOverviewInWindow()) {
+                mContainerInterface = FallbackWindowInterface.getInstance();
+            } else {
+                mContainerInterface = FallbackActivityInterface.INSTANCE;
+            }
             mIsHomeAndOverviewSame = false;
             mOverviewIntent = mFallbackIntent;
             mCurrentHomeIntent.setComponent(defaultHome);
@@ -266,21 +271,12 @@
     }
 
     /**
-     * Get the current activity control helper for managing interactions to the overview activity.
+     * Get the current control helper for managing interactions to the overview container.
      *
-     * @return the current activity control helper
+     * @return the current control helper
      */
-    public BaseActivityInterface getActivityInterface() {
-        return mActivityInterface;
-    }
-
-    /**
-     * Get the current container control helper for managing interactions to the overview activity.
-     *
-     * @return the current container control helper
-     */
-    public BaseContainerInterface<?, ?> getContainerInterface() {
-        return mActivityInterface;
+    public BaseContainerInterface<?,?> getContainerInterface() {
+        return mContainerInterface;
     }
 
     public void dump(PrintWriter pw) {
@@ -309,10 +305,11 @@
      * Starts the intent for the current home activity.
      */
     public static void startHomeIntentSafely(
-            @NonNull Context context, @NonNull Intent homeIntent, @Nullable Bundle options,
+            @NonNull Context context,
+            @NonNull Intent homeIntent,
+            @Nullable Bundle options,
             @NonNull String reason) {
-        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                "OverviewComponentObserver.startHomeIntent: ").append(reason));
+        ActiveGestureProtoLogProxy.logStartHomeIntent(reason);
         try {
             context.startActivity(homeIntent, options);
         } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index f4e68dc..334bead 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -25,6 +25,7 @@
 
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.MainProcessInitializer;
+import com.android.quickstep.util.QuickstepProtoLogGroup;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 @SuppressWarnings("unused")
@@ -69,5 +70,7 @@
                     call.descriptor + " called on main thread under " + call.activeTrace
                             + " stackTrace: " + call.stackTrace));
         }
+
+        QuickstepProtoLogGroup.initProtoLog();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 49b6f57..c3b9736 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -7,6 +7,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Bundle;
+import android.view.WindowInsets;
 
 import androidx.annotation.Nullable;
 
@@ -203,11 +204,12 @@
     }
 
     @Override
-    protected Activity getCurrentActivity() {
+    protected WindowInsets getWindowInsets() {
         RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
         OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
         try {
-            return observer.getActivityInterface().getCreatedContainer();
+            return observer.getContainerInterface()
+                    .getCreatedContainer().getRootView().getRootWindowInsets();
         } finally {
             observer.onDestroy();
             rads.destroy();
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 6d5cb4b..9ac4141 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -63,6 +63,7 @@
 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;
@@ -121,9 +122,6 @@
     // 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
@@ -136,7 +134,7 @@
         SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
         // SplitSelectStateController needs to be created before setContentView()
         mSplitSelectStateController =
-                new SplitSelectStateController(this, mHandler, getStateManager(),
+                new SplitSelectStateController(this, getStateManager(),
                         null /* depthController */, getStatsLogManager(),
                         systemUiProxy, RecentsModel.INSTANCE.get(this),
                         null /*activityBackCallback*/);
@@ -197,7 +195,7 @@
     }
 
     @Override
-    protected void onHandleConfigurationChanged() {
+    public void onHandleConfigurationChanged() {
         initDeviceProfile();
 
         AbstractFloatingView.closeOpenViews(this, true,
@@ -351,7 +349,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();
@@ -525,4 +522,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 7b9b560..fc11812 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -19,9 +19,6 @@
 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;
@@ -34,8 +31,7 @@
 
 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;
 
@@ -107,11 +103,8 @@
                 .filter(app -> app.mode == MODE_CLOSING)
                 .count();
         if (appCount == 0) {
+            ActiveGestureProtoLogProxy.logOnRecentsAnimationStartCancelled();
             // Edge case, if there are no closing app targets, then Launcher has nothing to handle
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "RecentsAnimationCallbacks.onAnimationStart (canceled)",
-                    /* extras= */ 0,
-                    /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
             notifyAnimationCanceled();
             animationController.finish(false /* toHome */, false /* sendUserLeaveHint */,
                     null /* finishCb */);
@@ -138,10 +131,7 @@
                     extras);
 
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-                ActiveGestureLog.INSTANCE.addLog(
-                        /* event= */ "RecentsAnimationCallbacks.onAnimationStart",
-                        /* extras= */ targets.apps.length,
-                        /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+                ActiveGestureProtoLogProxy.logOnRecentsAnimationStart(targets.apps.length);
                 for (RecentsAnimationListener listener : getListeners()) {
                     listener.onRecentsAnimationStart(mController, targets);
                 }
@@ -153,9 +143,7 @@
     @Override
     public final void onAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled",
-                    /* gestureEvent= */ ON_CANCEL_RECENTS_ANIMATION);
+            ActiveGestureProtoLogProxy.logRecentsAnimationCallbacksOnAnimationCancelled();
             for (RecentsAnimationListener listener : getListeners()) {
                 listener.onRecentsAnimationCanceled(thumbnailDatas);
             }
@@ -166,31 +154,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);
             }
@@ -242,12 +215,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 adcf4ef..dcb0108 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -17,13 +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.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;
@@ -34,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;
@@ -90,15 +87,6 @@
         }
     }
 
-    /**
-     * 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 */);
@@ -143,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;
@@ -155,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();
                     });
@@ -173,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
@@ -194,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
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index a01ceb2..d073580 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -56,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;
@@ -76,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;
@@ -239,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;
             }
@@ -269,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);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 1be60de..8adc11a 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -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) {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index dde16c8..34435d5 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
 
 import android.app.ActivityManager;
@@ -43,8 +42,6 @@
 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.MotionEvent;
 import android.view.RemoteAnimationTarget;
@@ -53,9 +50,11 @@
 import android.window.RemoteTransition;
 import android.window.TaskSnapshot;
 import android.window.TransitionFilter;
+import android.window.flags.DesktopModeFlags;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.internal.logging.InstanceId;
@@ -64,7 +63,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AssistUtils;
 import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -89,10 +88,11 @@
 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.GroupedRecentTaskInfo;
 import com.android.wm.shell.shared.IShellTransitions;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
-import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.shared.split.SplitBounds;
@@ -187,7 +187,8 @@
     @Nullable
     private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider;
 
-    private SystemUiProxy(Context context) {
+    @VisibleForTesting
+    protected SystemUiProxy(Context context) {
         mContext = context;
         mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync);
         final Intent baseIntent = new Intent().setPackage(mContext.getPackageName());
@@ -1395,7 +1396,7 @@
 
     private boolean shouldEnableRunningTasksForDesktopMode() {
         return DesktopModeStatus.canEnterDesktopMode(mContext)
-                && DesktopModeFlags.TASKBAR_RUNNING_APPS.isEnabled(mContext);
+                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue();
     }
 
     private boolean handleMessageAsync(Message msg) {
@@ -1518,7 +1519,7 @@
     public boolean startRecentsActivity(Intent intent, ActivityOptions options,
             RecentsAnimationListener listener) {
         if (mRecentTasks == null) {
-            ActiveGestureLog.INSTANCE.addLog("Null mRecentTasks", RECENT_TASKS_MISSING);
+            ActiveGestureProtoLogProxy.logRecentTasksMissing();
             return false;
         }
         final IRecentsAnimationRunner runner = new IRecentsAnimationRunner.Stub() {
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index da967ab..98d7628 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -26,7 +26,6 @@
 import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 
@@ -43,11 +42,13 @@
 import androidx.annotation.UiThread;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.DisplayController;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.SystemUiFlagUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -64,6 +65,7 @@
             SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
 
     private final Context mCtx;
+    private RecentsWindowManager mRecentsWindowsManager;
     private RecentsAnimationController mController;
     private RecentsAnimationCallbacks mCallbacks;
     private RecentsAnimationTargets mTargets;
@@ -98,10 +100,10 @@
         }
     };
 
-    TaskAnimationManager(Context ctx) {
+    TaskAnimationManager(Context ctx, RecentsWindowManager manager) {
         mCtx = ctx;
+        mRecentsWindowsManager = manager;
     }
-
     SystemUiProxy getSystemUiProxy() {
         return SystemUiProxy.INSTANCE.get(mCtx);
     }
@@ -134,9 +136,7 @@
     @UiThread
     public RecentsAnimationCallbacks startRecentsAnimation(@NonNull GestureState gestureState,
             Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "startRecentsAnimation",
-                /* gestureEvent= */ START_RECENTS_ANIMATION);
+        ActiveGestureProtoLogProxy.logStartRecentsAnimation();
         // Notify if recents animation is still running
         if (mController != null) {
             String msg = "New recents animation started before old animation completed";
@@ -166,9 +166,8 @@
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
                 if (enableHandleDelayedGestureCallbacks() && mRecentsAnimationStartPending) {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "TaskAnimationManager.startRecentsAnimation(onRecentsAnimationStart): ")
-                            .append("Setting mRecentsAnimationStartPending = false"));
+                    ActiveGestureProtoLogProxy.logStartRecentsAnimationCallback(
+                            "onRecentsAnimationStart");
                     mRecentsAnimationStartPending = false;
                 }
                 if (mCallbacks == null) {
@@ -212,10 +211,8 @@
             @Override
             public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
                 if (enableHandleDelayedGestureCallbacks() && mRecentsAnimationStartPending) {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "TaskAnimationManager.startRecentsAnimation")
-                            .append("(onRecentsAnimationCanceled): ")
-                            .append("Setting mRecentsAnimationStartPending = false"));
+                    ActiveGestureProtoLogProxy.logStartRecentsAnimationCallback(
+                            "onRecentsAnimationCanceled");
                     mRecentsAnimationStartPending = false;
                 }
                 cleanUpRecentsAnimation(newCallbacks);
@@ -224,10 +221,8 @@
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
                 if (enableHandleDelayedGestureCallbacks() && mRecentsAnimationStartPending) {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "TaskAnimationManager.startRecentsAnimation")
-                            .append("(onRecentsAnimationFinished): ")
-                            .append("Setting mRecentsAnimationStartPending = false"));
+                    ActiveGestureProtoLogProxy.logStartRecentsAnimationCallback(
+                            "onRecentsAnimationFinished");
                     mRecentsAnimationStartPending = false;
                 }
                 cleanUpRecentsAnimation(newCallbacks);
@@ -273,93 +268,70 @@
                     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);
 
         final ActivityOptions options = ActivityOptions.makeBasic();
-        options.setPendingIntentBackgroundActivityStartMode(
-                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
-        options.setTransientLaunch();
-        options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
 
-        // Notify taskbar that we should skip reacting to launcher visibility change to
-        // avoid a jumping taskbar.
-        TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
-        if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
-            taskbarUIController.setSkipLauncherVisibilityChange(true);
+        // TODO:(b/365777482) if flag is enabled, but on launcher it will crash.
+        if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
+                && Flags.enableFallbackOverviewInWindow()){
+            mRecentsAnimationStartPending =
+                    getSystemUiProxy().startRecentsActivity(intent, options, mCallbacks);
+            mRecentsWindowsManager.startRecentsWindow();
+        } else {
+            options.setPendingIntentBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+            options.setTransientLaunch();
+            options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
 
-            mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
-                @Override
-                public void onRecentsAnimationCanceled(
-                        @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
-                    taskbarUIController.setSkipLauncherVisibilityChange(false);
-                }
+            // Notify taskbar that we should skip reacting to launcher visibility change to
+            // avoid a jumping taskbar.
+            TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+            if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+                taskbarUIController.setSkipLauncherVisibilityChange(true);
 
-                @Override
-                public void onRecentsAnimationFinished(
-                        @NonNull RecentsAnimationController controller) {
-                    taskbarUIController.setSkipLauncherVisibilityChange(false);
-                }
-            });
+                mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+                    @Override
+                    public void onRecentsAnimationCanceled(
+                            @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+
+                    @Override
+                    public void onRecentsAnimationFinished(
+                            @NonNull RecentsAnimationController controller) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+                });
+            }
+
+            mRecentsAnimationStartPending = getSystemUiProxy()
+                    .startRecentsActivity(intent, options, mCallbacks);
         }
-
-        mRecentsAnimationStartPending = getSystemUiProxy()
-                .startRecentsActivity(intent, options, mCallbacks);
         if (enableHandleDelayedGestureCallbacks()) {
-            ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                    "TaskAnimationManager.startRecentsAnimation: ")
-                    .append("Setting mRecentsAnimationStartPending = ")
-                    .append(mRecentsAnimationStartPending));
+            ActiveGestureProtoLogProxy.logSettingRecentsAnimationStartPending(
+                    mRecentsAnimationStartPending);
         }
         gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED);
         return mCallbacks;
@@ -369,7 +341,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);
@@ -451,8 +423,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 */);
@@ -489,11 +460,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;
@@ -515,6 +485,10 @@
         mTargets = null;
         mLastGestureState = null;
         mLastAppearedTaskTargets = null;
+
+        if(Flags.enableFallbackOverviewInWindow()) {
+            mRecentsWindowsManager.cleanup();
+        }
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 1f6c02c..91fa72d 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -226,7 +226,7 @@
         synchronized (mDefaultIcons) {
             if (mDefaultIconBase == null) {
                 try (BaseIconFactory bif = getIconFactory()) {
-                    mDefaultIconBase = bif.makeDefaultIcon();
+                    mDefaultIconBase = bif.makeDefaultIcon(mIconProvider);
                 }
             }
 
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index f4ff4b2..8e45767 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -173,9 +173,8 @@
 
         protected T getActionsView() {
             if (mActionsView == null) {
-                mActionsView = BaseActivity.fromContext(
-                        mTaskContainer.getTaskView().getContext()).findViewById(
-                        R.id.overview_actions_view);
+                mActionsView = (T) RecentsViewContainer.containerFromContext(
+                        mTaskContainer.getTaskView().getContext()).getActionsView();
             }
             return mActionsView;
         }
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 63e536a..49f7daf 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -31,8 +31,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.TraceHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -70,8 +70,8 @@
             return "";
         }
         UserHandle user = UserHandle.of(userId);
-        ApplicationInfo applicationInfo = PackageManagerHelper.INSTANCE.get(context)
-                .getApplicationInfo(packageName, user, 0);
+        ApplicationInfo applicationInfo =
+                new ApplicationInfoWrapper(context, packageName, user).getInfo();
         if (applicationInfo == null) {
             Log.e(TAG, "Failed to get title for userId=" + userId + ", packageName=" + packageName);
             return "";
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 1a09691..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();
@@ -304,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;
         }
@@ -425,7 +420,8 @@
 
         if (depthController != null) {
             out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
-                    BACKGROUND_APP.getDepth(baseActivity), TOUCH_RESPONSE);
+                    BACKGROUND_APP.getDepth(container),
+                    TOUCH_RESPONSE);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 3cf0542..23a1ec7 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -206,12 +206,17 @@
             Collections.addAll(mOrderedTaskList, tasks);
         }
 
-        // Strip the pinned task
         ArrayList<RunningTaskInfo> tasks = new ArrayList<>(mOrderedTaskList);
-        tasks.removeIf(t -> t.taskId == mPinnedTaskId);
+        // Strip the pinned task and recents task
+        tasks.removeIf(t -> t.taskId == mPinnedTaskId || isRecentsTask(t));
         return new CachedTaskInfo(tasks);
     }
 
+    private static boolean isRecentsTask(RunningTaskInfo task) {
+        return task != null && task.configuration.windowConfiguration
+                .getActivityType() == ACTIVITY_TYPE_RECENTS;
+    }
+
     /**
      * Class to provide information about a task which can be safely cached and do not change
      * during the lifecycle of the task.
@@ -267,8 +272,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 98e7fed..f0943dc 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,11 +36,6 @@
 import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
 import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
@@ -79,6 +71,7 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.MotionEvent;
+import android.view.View;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
@@ -86,14 +79,13 @@
 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.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;
@@ -110,6 +102,8 @@
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.OverviewCommandHelper.CommandType;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
@@ -127,6 +121,7 @@
 import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AssistStateManager;
 import com.android.quickstep.util.AssistUtils;
 import com.android.quickstep.views.RecentsViewContainer;
@@ -227,7 +222,6 @@
         @BinderThread
         @Override
         public void onTaskbarToggled() {
-            if (!FeatureFlags.ENABLE_KEYBOARD_TASKBAR_TOGGLE.get()) return;
             MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
                 TaskbarActivityContext activityContext =
                         tis.mTaskbarManager.getCurrentActivityContext();
@@ -328,10 +322,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);
                 }
             });
         }
@@ -462,6 +456,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();
@@ -598,6 +604,8 @@
             this::createLauncherSwipeHandler;
     private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
             this::createFallbackSwipeHandler;
+    private final AbsSwipeUpHandler.Factory mRecentsWindowSwipeHandlerFactory =
+            this::createRecentsWindowSwipeHandler;
 
     private final ScreenOnTracker.ScreenOnListener mScreenOnListener = this::onScreenOnChanged;
 
@@ -630,6 +638,7 @@
     private InputEventReceiver mInputEventReceiver;
 
     private TaskbarManager mTaskbarManager;
+    private RecentsWindowManager mRecentsWindowManager;
     private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
     private AllAppsActionManager mAllAppsActionManager;
     private InputManager mInputManager;
@@ -637,9 +646,13 @@
 
     private NavigationMode mGestureStartNavMode = null;
 
+    private DesktopVisibilityController mDesktopVisibilityController;
+
     @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();
@@ -649,15 +662,18 @@
         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);
+        if(Flags.enableFallbackOverviewInWindow()) {
+            mRecentsWindowManager = new RecentsWindowManager(this);
+        }
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
 
         // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized.
@@ -670,7 +686,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;
@@ -686,7 +703,7 @@
 
         if (mDeviceState.isButtonNavMode()
                 && !mDeviceState.supportsAssistantGestureInButtonNav()
-                && (!ENABLE_TRACKPAD_GESTURE.get() || mTrackpadsConnected.isEmpty())) {
+                && (mTrackpadsConnected.isEmpty())) {
             return;
         }
 
@@ -707,8 +724,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);
@@ -751,33 +769,25 @@
 
     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
+                && newOverviewContainer instanceof StatefulActivity activity) {
+            //TODO(b/368030750) refactor taskbarManager to accept RecentsViewContainer
+            mTaskbarManager.setActivity(activity);
         }
         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
@@ -794,14 +804,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();
@@ -817,6 +828,11 @@
         mTrackpadsConnected.clear();
 
         mTaskbarManager.destroy();
+
+        if (mRecentsWindowManager != null) {
+            mRecentsWindowManager.destroy();
+        }
+        mDesktopVisibilityController.onDestroy();
         sConnected = false;
 
         ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener);
@@ -825,7 +841,8 @@
 
     @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;
     }
 
@@ -842,9 +859,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;
@@ -853,27 +868,19 @@
                 TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
 
         if (!LockedUserState.get(this).isUserUnlocked()) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event: user is locked"));
+            ActiveGestureProtoLogProxy.logOnInputEventUserLocked();
             return;
         }
 
         NavigationMode currentNavMode = mDeviceState.getMode();
         if (mGestureStartNavMode != null && mGestureStartNavMode != currentNavMode) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                            .append("Navigation mode switched mid-gesture (")
-                            .append(mGestureStartNavMode.name())
-                            .append(" -> ")
-                            .append(currentNavMode.name())
-                            .append("); cancelling gesture."),
-                    NAVIGATION_MODE_SWITCHED);
+            ActiveGestureProtoLogProxy.logOnInputEventNavModeSwitched(
+                    mGestureStartNavMode.name(), currentNavMode.name());
             event.setAction(ACTION_CANCEL);
         } else if (mDeviceState.isButtonNavMode()
                 && !mDeviceState.supportsAssistantGestureInButtonNav()
                 && !isTrackpadMotionEvent(event)) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event: ")
-                    .append("using 3-button nav and event is not a trackpad event"));
+            ActiveGestureProtoLogProxy.logOnInputEventThreeButtonNav();
             return;
         }
 
@@ -889,12 +896,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;
             }
@@ -909,7 +911,7 @@
         SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
 
         CompoundString reasonString = action == ACTION_DOWN
-                ? new CompoundString("TIS.onMotionEvent: ") : CompoundString.NO_OP;
+                ? CompoundString.newEmptyString() : CompoundString.NO_OP;
         if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
             mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
@@ -927,22 +929,22 @@
                 reasonString.append("in three button mode which supports Assistant gesture");
                 // Consume gesture event for Assistant (all other gestures should do nothing).
                 if (mDeviceState.canTriggerAssistantAction(event)) {
-                    reasonString.append(" and event can trigger assistant action")
-                            .append(", consuming gesture for assistant action");
+                    reasonString.append(" and event can trigger assistant action, "
+                            + "consuming gesture for assistant action");
                     mGestureState =
                             createGestureState(mGestureState, getTrackpadGestureType(event));
                     mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
                 } else {
-                    reasonString.append(" but event cannot trigger Assistant")
-                            .append(", consuming gesture as no-op");
+                    reasonString.append(" but event cannot trigger Assistant, "
+                            + "consuming gesture as no-op");
                     mUncheckedConsumer = InputConsumer.NO_OP;
                 }
             } else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
                     || isHoverActionWithoutConsumer || isOnBubbles) {
                 reasonString.append(!isOneHandedModeActive && isInSwipeUpTouchRegion
-                                ? "one handed mode is not active and event is in swipe up region"
-                                : "isHoverActionWithoutConsumer == true")
-                        .append(", creating new input consumer");
+                        ? "one handed mode is not active and event is in swipe up region, "
+                                + "creating new input consumer"
+                        : "isHoverActionWithoutConsumer == true, creating new input consumer");
                 // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
                 // onConsumerInactive and wipe the previous gesture state
                 GestureState prevGestureState = new GestureState(mGestureState);
@@ -955,18 +957,18 @@
             } else if ((mDeviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
                     && mDeviceState.canTriggerAssistantAction(event)) {
                 reasonString.append(mDeviceState.isFullyGesturalNavMode()
-                                ? "using fully gestural nav"
-                                : "event is a trackpad multi-finger swipe")
-                        .append(" and event can trigger assistant action")
-                        .append(", consuming gesture for assistant action");
+                        ? "using fully gestural nav and event can trigger assistant action, "
+                                + "consuming gesture for assistant action"
+                        : "event is a trackpad multi-finger swipe and event can trigger assistant "
+                                + "action, consuming gesture for assistant action");
                 mGestureState = createGestureState(mGestureState, getTrackpadGestureType(event));
                 // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
                 // should not interrupt it. QuickSwitch assumes that interruption can only
                 // happen if the next gesture is also quick switch.
                 mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
             } else if (mDeviceState.canTriggerOneHandedAction(event)) {
-                reasonString.append("event can trigger one-handed action")
-                                .append(", consuming gesture for one-handed action");
+                reasonString.append("event can trigger one-handed action, "
+                        + "consuming gesture for one-handed action");
                 // Consume gesture event for triggering one handed feature.
                 mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
                         InputConsumer.NO_OP, mInputMonitorCompat);
@@ -984,41 +986,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()));
                 }
             }
         }
@@ -1073,11 +1059,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);
         }
@@ -1108,10 +1094,8 @@
         gestureState.setTrackpadGestureType(trackpadGestureType);
 
         // Log initial state for the gesture.
-        ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current running task package name=")
-                .append(taskInfo.getPackageName()));
-        ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current SystemUi state flags=")
-                .append(mDeviceState.getSystemUiStateString()));
+        ActiveGestureProtoLogProxy.logRunningTaskPackage(taskInfo.getPackageName());
+        ActiveGestureProtoLogProxy.logSysuiStateFlags(mDeviceState.getSystemUiStateString());
         return gestureState;
     }
 
@@ -1148,12 +1132,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;
@@ -1165,13 +1148,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()) {
@@ -1181,11 +1163,11 @@
             String reasonPrefix =
                     "device is in gesture navigation mode or 3-button mode with a trackpad gesture";
             if (mDeviceState.canTriggerAssistantAction(event)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("gesture can trigger the assistant")
-                        .append(", trying to use assistant input consumer");
+                reasonString.append("%s%s%sgesture can trigger the assistant, "
+                                + "trying to use assistant input consumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString);
             }
 
@@ -1196,11 +1178,11 @@
                         && !tac.isPhoneMode()
                         && !tac.isInStashedLauncherState();
                 if (canStartSystemGesture && useTaskbarConsumer) {
-                    reasonString.append(NEWLINE_PREFIX)
-                            .append(reasonPrefix)
-                            .append(SUBSTRING_PREFIX)
-                            .append("TaskbarActivityContext != null, ")
-                            .append("using TaskbarUnstashInputConsumer");
+                    reasonString.append("%s%s%sTaskbarActivityContext != null, "
+                                    + "using TaskbarUnstashInputConsumer",
+                            NEWLINE_PREFIX,
+                            reasonPrefix,
+                            SUBSTRING_PREFIX);
                     base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
                             mOverviewCommandHelper, mGestureState);
                 }
@@ -1209,9 +1191,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);
                 }
@@ -1222,10 +1204,10 @@
             if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
                     && navHandle.canNavHandleBeLongPressed()
                     && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("Not running recents animation, ");
+                reasonString.append("%s%s%sNot running recents animation, ",
+                                NEWLINE_PREFIX,
+                                reasonPrefix,
+                                SUBSTRING_PREFIX);
                 if (tac != null && tac.getNavHandle().canNavHandleBeLongPressed()) {
                     reasonString.append("stashed handle is long-pressable, ");
                 }
@@ -1237,74 +1219,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);
             }
@@ -1314,7 +1296,7 @@
     }
 
     private CompoundString newCompoundString(String substring) {
-        return new CompoundString(NEWLINE_PREFIX).append(substring);
+        return new CompoundString("%s%s", NEWLINE_PREFIX, substring);
     }
 
     private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
@@ -1324,10 +1306,7 @@
 
     private void logInputConsumerSelectionReason(
             InputConsumer consumer, CompoundString reasonString) {
-        ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ")
-                .append(consumer.getName())
-                .append(". reason(s):")
-                .append(reasonString));
+        ActiveGestureProtoLogProxy.logSetInputConsumer(consumer.getName(), reasonString.toString());
         if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
             ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER);
         }
@@ -1344,14 +1323,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.
@@ -1365,11 +1342,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);
         }
 
@@ -1395,11 +1369,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) {
@@ -1408,28 +1383,30 @@
                     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;
+        return mOverviewComponentObserver.isHomeAndOverviewSame()
+                ? mLauncherSwipeHandlerFactory : (Flags.enableFallbackOverviewInWindow()
+                ? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
     }
 
     private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
@@ -1448,20 +1425,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));
         }
     }
 
@@ -1473,35 +1448,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);
@@ -1538,12 +1513,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;
@@ -1573,11 +1550,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.
@@ -1587,8 +1564,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);
     }
 
@@ -1597,18 +1573,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()
@@ -1647,11 +1623,11 @@
         pw.println("\tmInputEventReceiver=" + mInputEventReceiver);
         DisplayController.INSTANCE.get(this).dump(pw);
         pw.println("TouchState:");
-        BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
-                : mOverviewComponentObserver.getActivityInterface().getCreatedContainer();
+        RecentsViewContainer createdOverviewContainer = mOverviewComponentObserver == null ? null
+                : mOverviewComponentObserver.getContainerInterface().getCreatedContainer();
         boolean resumed = mOverviewComponentObserver != null
-                && mOverviewComponentObserver.getActivityInterface().isResumed();
-        pw.println("\tcreatedOverviewActivity=" + createdOverviewActivity);
+                && mOverviewComponentObserver.getContainerInterface().isResumed();
+        pw.println("\tcreatedOverviewActivity=" + createdOverviewContainer);
         pw.println("\tresumed=" + resumed);
         pw.println("\tmConsumer=" + mConsumer.getName());
         ActiveGestureLog.INSTANCE.dump("", pw);
@@ -1659,10 +1635,11 @@
         if (mTaskAnimationManager != null) {
             mTaskAnimationManager.dump("", pw);
         }
-        if (createdOverviewActivity != null) {
-            createdOverviewActivity.getDeviceProfile().dump(this, "", pw);
+        if (createdOverviewContainer != null) {
+            createdOverviewContainer.getDeviceProfile().dump(this, "", pw);
         }
         mTaskbarManager.dumpLogs("", pw);
+        mDesktopVisibilityController.dumpLogs("", pw);
         pw.println("AssistStateManager:");
         AssistStateManager.INSTANCE.get(this).dump("\t", pw);
         SystemUiProxy.INSTANCE.get(this).dump(pw);
@@ -1682,4 +1659,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/dagger/QuickStepModule.java b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
new file mode 100644
index 0000000..08345b8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 dagger.Module;
+
+@Module
+public class QuickStepModule {
+}
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..f2d5715
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.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.quickstep.dagger;
+
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+import com.android.quickstep.logging.SettingsChangeLogger;
+
+/**
+ * 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 {
+    SettingsChangeLogger getSettingsChangeLogger();
+}
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..5a4c769 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()
+                ? 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,8 @@
             }
         }
 
-        if (isOverlayEnabled) {
+        // disabling this so app icons aren't drawn on top of recent tasks.
+        if (isOverlayEnabled && !Flags.enableFallbackOverviewInWindow()) {
             runActionOnRemoteHandles(remoteTargetHandle ->
                     remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
@@ -309,7 +323,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..082b96c 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -42,6 +42,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 +52,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 +150,11 @@
         return hasFlag(FLAG_TASK_THUMBNAIL_SPLASH);
     }
 
+    @Override
+    public boolean detachDesktopCarousel() {
+        return hasFlag(FLAG_DETACH_DESKTOP_CAROUSEL);
+    }
+
     /**
      * 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..8ce61f5
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.util.Log
+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.Window
+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.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.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.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 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
+ */
+class RecentsWindowManager(context: Context) :
+    RecentsWindowContext(context), RecentsViewContainer, StatefulContainer<RecentsState> {
+
+    companion object {
+        private const val HOME_APPEAR_DURATION: Long = 250
+        private const val TAG = "RecentsWindowManager"
+        private const val DEBUG = false
+    }
+
+    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 isShown = false
+
+    private var tisBindHelper: TISBindHelper = TISBindHelper(this) {}
+
+    // Callback array that corresponds to events defined in @ActivityEvent
+    private val mEventCallbacks =
+        arrayOf(RunnableList(), RunnableList(), RunnableList(), RunnableList())
+    private var onInitListener: Predicate<Boolean>? = null
+
+    init {
+        FallbackWindowInterface.init(this)
+    }
+
+    override fun destroy() {
+        super.destroy()
+        FallbackWindowInterface.getInstance()?.destroy()
+    }
+
+    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)
+    }
+
+    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(HOME, false)
+                    cleanup()
+                },
+                true, /* skipFirstFrame */
+            )
+        }
+
+    fun cleanup() {
+        if (isShown) {
+            windowManager.removeViewImmediate(windowView)
+            isShown = false
+        }
+    }
+
+    fun startRecentsWindow() {
+        if (isShown) return
+        if (windowView == null) {
+            windowView = layoutInflater.inflate(R.layout.fallback_recents_activity, null)
+        }
+        windowManager.addView(windowView, windowLayoutParams)
+        isShown = true
+
+        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)
+        onInitListener?.test(true)
+    }
+
+    override fun canStartHomeSafely(): Boolean {
+        val overviewCommandHelper = tisBindHelper.overviewCommandHelper
+        return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely()
+    }
+
+    override fun getDesktopVisibilityController(): DesktopVisibilityController? {
+        return tisBindHelper.desktopVisibilityController
+    }
+
+    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)
+        logState(state, "state started:")
+    }
+
+    override fun onStateSetEnd(state: RecentsState?) {
+        super.onStateSetEnd(state)
+        logState(state, "state ended:")
+    }
+
+    private fun logState(state: RecentsState?, prefix: String) {
+        if (!DEBUG) {
+            return
+        }
+        if (state != null) {
+            when (state) {
+                DEFAULT -> Log.d(TAG, prefix + "default")
+                MODAL_TASK -> {
+                    Log.d(TAG, prefix + "MODAL_TASK")
+                }
+                BACKGROUND_APP -> {
+                    Log.d(TAG, prefix + "BACKGROUND_APP")
+                }
+                HOME -> {
+                    Log.d(TAG, prefix + "HOME")
+                }
+                BG_LAUNCHER -> {
+                    Log.d(TAG, prefix + "BG_LAUNCHER")
+                }
+                OVERVIEW_SPLIT_SELECT -> {
+                    Log.d(TAG, prefix + "OVERVIEW_SPLIT_SELECT")
+                }
+            }
+        }
+    }
+
+    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 isShown
+    }
+
+    /** 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..34b3d74
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.animation.ObjectAnimator;
+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.RecentsAnimationDeviceState;
+import com.android.quickstep.RemoteAnimationTargets;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.util.TransformParams.BuilderProxy;
+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();
+        if (mRunningOverHome) {
+            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) {
+        setHomeScaleAndAlpha(builder, app, mCurrentShift.value,
+                Utilities.boundToRange(1 - mCurrentShift.value, 0, 1));
+    }
+
+    private void setHomeScaleAndAlpha(SurfaceProperties builder,
+            RemoteAnimationTarget app, float verticalShift, float alpha) {
+        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 (mActiveAnimationFactory != null && runningTaskTarget != null) {
+            mActiveAnimationFactory.addGestureContract(intent, runningTaskTarget.taskInfo);
+        }
+        return mActiveAnimationFactory;
+    }
+
+    @Override
+    protected boolean handleTaskAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets,
+            @NonNull ActiveGestureLog.CompoundString failureReason) {
+        if (mActiveAnimationFactory != null
+                && mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTargets)) {
+            mActiveAnimationFactory = null;
+            return false;
+        }
+
+        return super.handleTaskAppeared(appearedTaskTargets, failureReason);
+    }
+
+    @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 mHomeAlphaParams = new TransformParams();
+        private final AnimatedFloat mHomeAlpha;
+
+        private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat();
+        private final AnimatedFloat mRecentsAlpha = new AnimatedFloat();
+
+        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) {
+                mHomeAlpha = new AnimatedFloat();
+                mHomeAlpha.value = Utilities.boundToRange(1 - mCurrentShift.value, 0, 1);
+                mVerticalShiftForScale.value = mCurrentShift.value;
+                runActionOnRemoteHandles(remoteTargetHandle ->
+                        remoteTargetHandle.getTransformParams().setHomeBuilderProxy(
+                                FallbackHomeAnimationFactory.this
+                                        ::updateHomeActivityTransformDuringHomeAnim));
+            } else {
+                mHomeAlpha = new AnimatedFloat(this::updateHomeAlpha);
+                mHomeAlpha.value = 0;
+                mHomeAlphaParams.setHomeBuilderProxy(
+                        this::updateHomeActivityTransformDuringHomeAnim);
+            }
+
+            mRecentsAlpha.value = 1;
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTransformParams().setBaseBuilderProxy(
+                            FallbackHomeAnimationFactory.this
+                                    ::updateRecentsActivityTransformDuringHomeAnim));
+        }
+
+        @NonNull
+        @Override
+        public RectF getWindowTargetRect() {
+            if (mTargetRect.isEmpty()) {
+                mTargetRect.set(super.getWindowTargetRect());
+            }
+            return mTargetRect;
+        }
+
+        private void updateRecentsActivityTransformDuringHomeAnim(SurfaceProperties builder,
+                RemoteAnimationTarget app, TransformParams params) {
+            builder.setAlpha(mRecentsAlpha.value);
+        }
+
+        private void updateHomeActivityTransformDuringHomeAnim(SurfaceProperties builder,
+                RemoteAnimationTarget app, TransformParams params) {
+            setHomeScaleAndAlpha(builder, app, mVerticalShiftForScale.value, mHomeAlpha.value);
+        }
+
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            PendingAnimation pa = new PendingAnimation(mDuration);
+            pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCELERATE);
+            return pa.createPlaybackController();
+        }
+
+        private void updateHomeAlpha() {
+            if (mHomeAlphaParams.getTargetSet() != null) {
+                mHomeAlphaParams.applySurfaceParams(
+                        mHomeAlphaParams.createSurfaceParams(BuilderProxy.NO_OP));
+            }
+        }
+
+        public boolean handleHomeTaskAppeared(RemoteAnimationTarget[] appearedTaskTargets) {
+            RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
+            if (appearedTaskTarget.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME) {
+                RemoteAnimationTargets targets = new RemoteAnimationTargets(
+                        new RemoteAnimationTarget[] {appearedTaskTarget},
+                        new RemoteAnimationTarget[0], new RemoteAnimationTarget[0],
+                        appearedTaskTarget.mode);
+                mHomeAlphaParams.setTargetSet(targets);
+                updateHomeAlpha();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void playAtomicAnimation(float velocity) {
+            ObjectAnimator alphaAnim = mHomeAlpha.animateToValue(mHomeAlpha.value, 1);
+            alphaAnim.setDuration(mDuration).setInterpolator(ACCELERATE);
+            alphaAnim.start();
+
+            if (mRunningOverHome) {
+                // 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 onRectAnimationEnd() {
+            mAnimationFinished = true;
+            maybeSendEndMessage();
+        }
+
+        private void maybeSendEndMessage() {
+            if (mAnimationFinished && mOnFinishCallback != null) {
+                try {
+                    mOnFinishCallback.replyTo.send(mOnFinishCallback);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error sending icon position", e);
+                }
+            }
+        }
+
+        @Override
+        public void accept(Message msg) {
+            try {
+                Bundle data = msg.getData();
+                RectF position = data.getParcelable(EXTRA_ICON_POSITION);
+                if (!position.isEmpty()) {
+                    mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE);
+                    mTargetRect.set(position);
+                    if (mSpringAnim != null) {
+                        mSpringAnim.onTargetPositionChanged();
+                    }
+                }
+                mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK);
+                maybeSendEndMessage();
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+
+        @Override
+        public void update(RectF currentRect, float progress, float radius, int overlayAlpha) {
+            if (mSurfaceControl != null) {
+                currentRect.roundOut(mTempRect);
+                Transaction t = new Transaction();
+                try {
+                    t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+                    t.apply();
+                } catch (RuntimeException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        private void addGestureContract(Intent intent, RunningTaskInfo runningTaskInfo) {
+            if (mRunningOverHome || runningTaskInfo == null) {
+                return;
+            }
+
+            TaskKey key = new TaskKey(runningTaskInfo);
+            if (key.getComponent() != null) {
+                if (sMessageReceiver == null) {
+                    sMessageReceiver = new StaticMessageReceiver();
+                }
+
+                Bundle gestureNavContract = new Bundle();
+                gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent());
+                gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId));
+                gestureNavContract.putParcelable(
+                        EXTRA_REMOTE_CALLBACK, sMessageReceiver.newCallback(this));
+                intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract);
+            }
+        }
+    }
+
+    private static class StaticMessageReceiver implements Handler.Callback {
+
+        private final Messenger mMessenger =
+                new Messenger(new Handler(Looper.getMainLooper(), this));
+
+        private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID());
+        private WeakReference<Consumer<Message>> mCurrentCallback = new WeakReference<>(null);
+
+        public Message newCallback(Consumer<Message> callback) {
+            mCurrentUID = new ParcelUuid(UUID.randomUUID());
+            mCurrentCallback = new WeakReference<>(callback);
+
+            Message msg = Message.obtain();
+            msg.replyTo = mMessenger;
+            msg.obj = mCurrentUID;
+            return msg;
+        }
+
+        @Override
+        public boolean handleMessage(@NonNull Message message) {
+            if (mCurrentUID.equals(message.obj)) {
+                Consumer<Message> consumer = mCurrentCallback.get();
+                if (consumer != null) {
+                    consumer.accept(message);
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 92031c5..778c231 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -23,10 +23,12 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
-import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -40,10 +42,11 @@
 
     private final BubbleStashController mBubbleStashController;
     private final BubbleBarViewController mBubbleBarViewController;
-    private final BubbleDragController mBubbleDragController;
+    @Nullable
+    private final BubbleBarSwipeController mBubbleBarSwipeController;
     private final InputMonitorCompat mInputMonitorCompat;
 
-    private boolean mSwipeUpOnBubbleHandle;
+    private boolean mPilfered;
     private boolean mPassedTouchSlop;
     private boolean mStashedOrCollapsedOnDown;
 
@@ -57,7 +60,8 @@
             InputMonitorCompat inputMonitorCompat) {
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
-        mBubbleDragController = bubbleControllers.bubbleDragController;
+        mBubbleBarSwipeController = bubbleControllers.bubbleBarSwipeController.orElse(null);
+
         mInputMonitorCompat = inputMonitorCompat;
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mTimeForTap = ViewConfiguration.getTapTimeout();
@@ -77,6 +81,9 @@
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
                 mStashedOrCollapsedOnDown = mBubbleStashController.isStashed() || isCollapsed();
+                if (mBubbleBarSwipeController != null) {
+                    mBubbleBarSwipeController.start();
+                }
                 break;
             case MotionEvent.ACTION_MOVE:
                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
@@ -90,11 +97,10 @@
                 if (!mPassedTouchSlop) {
                     mPassedTouchSlop = Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
                 }
-                if (mStashedOrCollapsedOnDown && !mSwipeUpOnBubbleHandle && mPassedTouchSlop) {
-                    boolean verticalGesture = Math.abs(dY) > Math.abs(dX);
-                    if (verticalGesture && !mBubbleDragController.isDragging()) {
-                        mSwipeUpOnBubbleHandle = true;
-                        mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
+                if (mBubbleBarSwipeController != null) {
+                    mBubbleBarSwipeController.swipeTo(dY);
+                    if (!mPilfered && mBubbleBarSwipeController.isSwipeGesture()) {
+                        mPilfered = true;
                         // Bubbles is handling the swipe so make sure no one else gets it.
                         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
                         mInputMonitorCompat.pilferPointers();
@@ -102,8 +108,10 @@
                 }
                 break;
             case MotionEvent.ACTION_UP:
+                boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
+                        && mBubbleBarSwipeController.isSwipeGesture();
                 boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap;
-                if (isWithinTapTime && !mSwipeUpOnBubbleHandle && !mPassedTouchSlop
+                if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
                         && mStashedOrCollapsedOnDown) {
                     // Taps on the handle / collapsed state should open the bar
                     mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
@@ -116,8 +124,11 @@
     }
 
     private void cleanupAfterMotionEvent() {
+        if (mBubbleBarSwipeController != null) {
+            mBubbleBarSwipeController.finish();
+        }
         mPassedTouchSlop = false;
-        mSwipeUpOnBubbleHandle = false;
+        mPilfered = false;
     }
 
     private boolean isCollapsed() {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 5557639..4afd92a 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -5,7 +5,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.InputConsumer;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public abstract class DelegateInputConsumer implements InputConsumer {
@@ -57,8 +57,7 @@
     }
 
     protected void setActive(MotionEvent ev) {
-        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(getDelegatorName())
-                .append(" became active"));
+        ActiveGestureProtoLogProxy.logInputConsumerBecameActive(getDelegatorName());
 
         mState = STATE_ACTIVE;
         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 69d3bc9..e19b338 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;
@@ -424,7 +425,10 @@
             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
+            Intent intent = new Intent(Flags.enableFallbackOverviewInWindow()
+                ? 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 9284e13..5ad55ae 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -184,7 +184,7 @@
                             if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold
                                     && !mGestureState.isInExtendedSlopRegion()) {
                                 mHasPassedTaskbarNavThreshold = true;
-                                mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+                                mTaskbarActivityContext.onSwipeToUnstashTaskbar(true);
                             }
 
                             if (dY < 0) {
@@ -287,7 +287,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);
             }
@@ -315,7 +315,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 acc9959..bc5cc15 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -37,7 +37,6 @@
 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;
@@ -79,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
@@ -101,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();
@@ -115,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() {
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 54653fa..5028da4 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,7 +48,6 @@
 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;
@@ -153,8 +148,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 +159,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 +253,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 +296,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 +356,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 +420,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 +431,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 +479,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 +501,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 +549,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 +599,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) {
@@ -727,9 +613,7 @@
             mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
                     mContext, 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(
@@ -738,19 +622,15 @@
             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());
-            }
+            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));
         }
     }
 
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/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 717f6c8..995635f 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -43,16 +43,21 @@
 
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.DeviceGridState;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ExecutorUtil;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -60,9 +65,12 @@
 import java.io.IOException;
 import java.util.Optional;
 
+import javax.inject.Inject;
+
 /**
  * Utility class to log launcher settings changes
  */
+@LauncherAppSingleton
 public class SettingsChangeLogger implements
         DisplayController.DisplayInfoChangeListener, OnSharedPreferenceChangeListener,
         SafeCloseable {
@@ -70,8 +78,8 @@
     /**
      * Singleton instance
      */
-    public static MainThreadInitializedObject<SettingsChangeLogger> INSTANCE =
-            new MainThreadInitializedObject<>(SettingsChangeLogger::new);
+    public static DaggerSingletonObject<SettingsChangeLogger> INSTANCE =
+            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getSettingsChangeLogger);
 
     private static final String TAG = "SettingsChangeLogger";
     private static final String BOOLEAN_PREF = "SwitchPreference";
@@ -84,25 +92,31 @@
     private StatsLogManager.LauncherEvent mNotificationDotsEvent;
     private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent;
 
-    private SettingsChangeLogger(Context context) {
-        this(context, StatsLogManager.newInstance(context));
+    @Inject
+    SettingsChangeLogger(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
+        this(context, StatsLogManager.newInstance(context), tracker);
     }
 
     @VisibleForTesting
-    SettingsChangeLogger(Context context, StatsLogManager statsLogManager) {
+    SettingsChangeLogger(Context context, StatsLogManager statsLogManager,
+            DaggerSingletonTracker tracker) {
         mContext = context;
         mStatsLogManager = statsLogManager;
         mLoggablePrefs = loadPrefKeys(context);
-        DisplayController.INSTANCE.get(context).addChangeListener(this);
-        mNavMode = DisplayController.getNavigationMode(context);
 
-        getPrefs(context).registerOnSharedPreferenceChangeListener(this);
-        getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
+        ExecutorUtil.executeSyncOnMainOrFail(() -> {
+            DisplayController.INSTANCE.get(context).addChangeListener(this);
+            mNavMode = DisplayController.getNavigationMode(context);
 
-        SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context);
-        mSettingsCache.register(NOTIFICATION_BADGING_URI,
-                this::onNotificationDotsChanged);
-        onNotificationDotsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI));
+            getPrefs(context).registerOnSharedPreferenceChangeListener(this);
+            getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
+
+            SettingsCache settingsCache = SettingsCache.INSTANCE.get(context);
+            settingsCache.register(NOTIFICATION_BADGING_URI,
+                    this::onNotificationDotsChanged);
+            onNotificationDotsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
+            tracker.addCloseable(this);
+        });
     }
 
     private static ArrayMap<String, LoggablePref> loadPrefKeys(Context context) {
@@ -209,6 +223,8 @@
     public void close() {
         getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
         getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
+        SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
+        settingsCache.unregister(NOTIFICATION_BADGING_URI, this::onNotificationDotsChanged);
     }
 
     @VisibleForTesting
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index 658975c..88ef0a8 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -453,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,
@@ -460,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
@@ -569,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,
@@ -579,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 cc022b2..c0b697d 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -530,10 +530,16 @@
         }
     }
 
+    /**
+     * @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 =
@@ -541,11 +547,10 @@
         FrameLayout.LayoutParams secondaryParams =
                 (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
 
-        // Reset margin and translations that aren't used in this method, but are used in other
+        // Reset margins that aren't used in this method, but are used in other
         // `RecentsPagedOrientationHandler` variants.
         secondaryParams.topMargin = 0;
         primaryParams.topMargin = spaceAboveSnapshot;
-        primarySnapshot.setTranslationY(0);
 
         int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
         float dividerScale = splitBoundsConfig.appsStackedVertically
@@ -553,28 +558,35 @@
                 : 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);
-        } else {
-            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);
         }
+
         primarySnapshot.measure(
                 View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
                 View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY));
@@ -582,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
@@ -662,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()
@@ -680,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.
@@ -716,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 06a0685..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
     )
 
     /**
@@ -235,7 +236,8 @@
         groupedTaskViewWidth: Int,
         isRtl: Boolean,
         deviceProfile: DeviceProfile,
-        splitConfig: SplitConfigurationOptions.SplitBounds
+        splitConfig: SplitConfigurationOptions.SplitBounds,
+        inSplitSelection: Boolean
     )
 
     /*
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index a972e8c..bc91911 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -277,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,
@@ -284,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
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
index a141e89..a45d194 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -23,6 +23,7 @@
 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 :
@@ -30,7 +31,7 @@
     /** Registers a callback for visuals relating to icons */
     fun registerTaskIconChangedCallback(
         taskKey: Task.TaskKey,
-        taskIconChangedCallback: TaskIconChangedCallback
+        taskIconChangedCallback: TaskIconChangedCallback,
     )
 
     /** Unregisters a callback for visuals relating to icons */
@@ -39,7 +40,7 @@
     /** Registers a callback for visuals relating to thumbnails */
     fun registerTaskThumbnailChangedCallback(
         taskKey: Task.TaskKey,
-        taskThumbnailChangedCallback: TaskThumbnailChangedCallback
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback,
     )
 
     /** Unregisters a callback for visuals relating to thumbnails */
@@ -66,31 +67,9 @@
     private val highResLoadingStateNotifier: HighResLoadingStateNotifier,
 ) : TaskVisualsChangedDelegate {
     private val taskIconChangedCallbacks =
-        mutableMapOf<Int, Pair<Task.TaskKey, TaskIconChangedCallback>>()
+        ConcurrentHashMap<Int, Pair<Task.TaskKey, TaskIconChangedCallback>>()
     private val taskThumbnailChangedCallbacks =
-        mutableMapOf<Int, Pair<Task.TaskKey, TaskThumbnailChangedCallback>>()
-    private var isListening = false
-
-    @Synchronized
-    private fun onCallbackRegistered() {
-        if (isListening) return
-
-        taskVisualsChangeNotifier.addThumbnailChangeListener(this)
-        highResLoadingStateNotifier.addCallback(this)
-        isListening = true
-    }
-
-    @Synchronized
-    private fun onCallbackUnregistered() {
-        if (!isListening) return
-
-        if (taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size == 0) {
-            taskVisualsChangeNotifier.removeThumbnailChangeListener(this)
-            highResLoadingStateNotifier.removeCallback(this)
-        }
-
-        isListening = false
-    }
+        ConcurrentHashMap<Int, Pair<Task.TaskKey, TaskThumbnailChangedCallback>>()
 
     override fun onTaskIconChanged(taskId: Int) {
         taskIconChangedCallbacks[taskId]?.let { (_, callback) -> callback.onTaskIconChanged() }
@@ -119,27 +98,48 @@
 
     override fun registerTaskIconChangedCallback(
         taskKey: Task.TaskKey,
-        taskIconChangedCallback: TaskIconChangedCallback
+        taskIconChangedCallback: TaskIconChangedCallback,
     ) {
-        taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback
-        onCallbackRegistered()
+        updateCallbacks {
+            taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback
+        }
     }
 
     override fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) {
-        taskIconChangedCallbacks.remove(taskKey.id)
-        onCallbackUnregistered()
+        updateCallbacks { taskIconChangedCallbacks.remove(taskKey.id) }
     }
 
     override fun registerTaskThumbnailChangedCallback(
         taskKey: Task.TaskKey,
-        taskThumbnailChangedCallback: TaskThumbnailChangedCallback
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback,
     ) {
-        taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback
-        onCallbackRegistered()
+        updateCallbacks {
+            taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback
+        }
     }
 
     override fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) {
-        taskThumbnailChangedCallbacks.remove(taskKey.id)
-        onCallbackUnregistered()
+        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 eb3c2d1..dc8d537 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -17,6 +17,7 @@
 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
@@ -106,7 +107,7 @@
                 }
             }
             .flowOn(dispatcherProvider.io)
-            .shareIn(recentsCoroutineScope, SharingStarted.WhileSubscribed(), replay = 1)
+            .shareIn(recentsCoroutineScope, SharingStarted.WhileSubscribed(5000), replay = 1)
 
     override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
         if (forceRefresh) {
@@ -122,6 +123,7 @@
         getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
 
     override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+        Log.d(TAG, "setVisibleTasks: $visibleTaskIdList")
         this.visibleTaskIds.value = visibleTaskIdList.toSet()
     }
 
@@ -185,7 +187,7 @@
                                 TaskIconQueryResponse(
                                     it.newDrawable().mutate(),
                                     contentDescription,
-                                    title
+                                    title,
                                 )
                             )
                         }
@@ -193,12 +195,16 @@
                 continuation.invokeOnCancellation { cancellableTask?.cancel() }
             }
         }
+
+    companion object {
+        private const val TAG = "TasksRepository"
+    }
 }
 
 data class TaskIconQueryResponse(
     val icon: Drawable,
     val contentDescription: String,
-    val title: String
+    val title: String,
 )
 
 private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 0a5544f..b53650e 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -66,7 +66,7 @@
             val taskVisualsChangedDelegate =
                 TaskVisualsChangedDelegateImpl(
                     recentsModel,
-                    recentsModel.thumbnailCache.highResLoadingState
+                    recentsModel.thumbnailCache.highResLoadingState,
                 )
             set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate)
 
@@ -79,7 +79,7 @@
                         iconCache,
                         taskVisualsChangedDelegate,
                         recentsCoroutineScope,
-                        ProductionDispatchers
+                        ProductionDispatchers,
                     )
                 }
             set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
@@ -155,7 +155,8 @@
         scopeId: RecentsScopeId,
         extras: RecentsDependenciesExtras,
     ): T {
-        log("createDependency ${modelClass.simpleName} with $scopeId and $extras", Log.WARN)
+        log("createDependency ${modelClass.simpleName} with $scopeId and $extras started", Log.WARN)
+        log("linked scopes: ${getScope(scopeId).scopeIdsLinked}")
         val instance: Any =
             when (modelClass) {
                 RecentTasksRepository::class.java -> {
@@ -166,7 +167,7 @@
                             iconCache,
                             get(),
                             get(),
-                            ProductionDispatchers
+                            ProductionDispatchers,
                         )
                     }
                 }
@@ -193,7 +194,7 @@
                         task = task,
                         recentsViewData = inject(),
                         recentTasksRepository = inject(),
-                        getThumbnailPositionUseCase = inject()
+                        getThumbnailPositionUseCase = inject(),
                     )
                 }
                 GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
@@ -203,7 +204,7 @@
                     GetThumbnailPositionUseCase(
                         deviceProfileRepository = inject(),
                         rotationStateRepository = inject(),
-                        tasksRepository = inject()
+                        tasksRepository = inject(),
                     )
                 SplashAlphaUseCase::class.java ->
                     SplashAlphaUseCase(
@@ -218,7 +219,12 @@
                     error("Factory for ${modelClass.simpleName} not defined!")
                 }
             }
-        return instance as T
+        return (instance as T).also {
+            log(
+                "createDependency ${modelClass.simpleName} with $scopeId and $extras completed",
+                Log.WARN,
+            )
+        }
     }
 
     private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
@@ -247,11 +253,7 @@
         fun initialize(view: View): RecentsDependencies = initialize(view.context)
 
         fun initialize(context: Context): RecentsDependencies {
-            synchronized(this) {
-                if (!Companion::instance.isInitialized) {
-                    instance = RecentsDependencies(context.applicationContext)
-                }
-            }
+            synchronized(this) { instance = RecentsDependencies(context.applicationContext) }
             return instance
         }
 
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 1716f2e..5cf6823 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -24,7 +24,7 @@
 
 class RecentsViewModel(
     private val recentsTasksRepository: RecentTasksRepository,
-    private val recentsViewData: RecentsViewData
+    private val recentsViewData: RecentsViewData,
 ) {
     fun refreshAllTaskData() {
         recentsTasksRepository.getAllTaskData(true)
@@ -58,7 +58,8 @@
         recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
     }
 
-    suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>) {
+    suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>?) {
+        if (updatedThumbnails.isNullOrEmpty()) return
         combine(
                 updatedThumbnails.map {
                     recentsTasksRepository.getThumbnailById(it.key).filter { thumbnailData ->
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 0279818..e7416ec 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -22,6 +22,7 @@
 import android.graphics.Outline
 import android.graphics.Rect
 import android.util.AttributeSet
+import android.util.Log
 import android.view.View
 import android.view.ViewOutlineProvider
 import androidx.annotation.ColorInt
@@ -92,6 +93,7 @@
             CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
         viewModel.uiState
             .onEach { viewModelUiState ->
+                Log.d(TAG, "viewModelUiState changed from $uiState to: $viewModelUiState")
                 uiState = viewModelUiState
                 resetViews()
                 when (viewModelUiState) {
@@ -211,6 +213,10 @@
         Utilities.mapRange(
             viewModel.cornerRadiusProgress.value,
             overviewCornerRadius,
-            fullscreenCornerRadius
+            fullscreenCornerRadius,
         ) / inheritedScale
+
+    private companion object {
+        const val TAG = "TaskThumbnailView"
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index b1bb65e..4970685 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -19,6 +19,7 @@
 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.quickstep.recents.data.RecentTasksRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
@@ -82,7 +83,7 @@
         combine(
                 task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
                 recentsViewData.runningTaskIds,
-                recentsViewData.runningTaskShowScreenshot
+                recentsViewData.runningTaskShowScreenshot,
             ) { taskId, runningTaskIds, runningTaskShowScreenshot ->
                 runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
             }
@@ -90,6 +91,13 @@
 
     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
@@ -103,6 +111,7 @@
             .distinctUntilChanged()
 
     fun bind(taskId: Int) {
+        Log.d(TAG, "bind taskId: $taskId")
         this.taskId = taskId
         task.value = tasksRepository.getTaskDataById(taskId)
         splashProgress.value = splashAlphaUseCase.execute(taskId)
@@ -139,5 +148,6 @@
 
     private companion object {
         const val MAX_SCRIM_ALPHA = 0.4f
+        const val TAG = "TaskThumbnailViewModel"
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index e1013db..7388d59 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -26,7 +26,7 @@
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
@@ -189,7 +189,7 @@
         @PersistentSnapPosition int snapPosition = gtv.getSnapPosition();
         if (snapPosition == SNAP_TO_NONE) {
             // Free snap mode is enabled, just save it as 50/50 split.
-            snapPosition = SNAP_TO_50_50;
+            snapPosition = SNAP_TO_2_50_50;
         }
         if (!isPersistentSnapPosition(snapPosition)) {
             // If we received an illegal snap position, log an error and do not create the app pair
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/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/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 b9338a3..a8460c9 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -23,7 +23,7 @@
 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 {
@@ -41,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);
     }
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index 15081da..8762e86 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -98,10 +98,8 @@
         mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
         mForcePauseTimeout = new Alarm();
         mForcePauseTimeout.setOnAlarmListener(alarm -> {
-            ActiveGestureLog.CompoundString log =
-                    new ActiveGestureLog.CompoundString("Force pause timeout after ")
-                            .append(alarm.getLastSetTimeout())
-                            .append("ms");
+            ActiveGestureLog.CompoundString log = new ActiveGestureLog.CompoundString(
+                    "Force pause timeout after %dms", alarm.getLastSetTimeout());
             addLogs(log);
             updatePaused(true /* isPaused */, log);
         });
@@ -124,9 +122,8 @@
      * @param disallowPause If true, we will not detect any pauses until this is set to false again.
      */
     public void setDisallowPause(boolean disallowPause) {
-        ActiveGestureLog.CompoundString log =
-                new ActiveGestureLog.CompoundString("Set disallowPause=")
-                        .append(disallowPause);
+        ActiveGestureLog.CompoundString log = new ActiveGestureLog.CompoundString(
+                "Set disallowPause=%b", disallowPause);
         if (mDisallowPause != disallowPause) {
             addLogs(log);
         }
@@ -188,8 +185,8 @@
                             speed < previousSpeed * getRapidDecelerationFactor();
                     isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
                     isPausedReason = new ActiveGestureLog.CompoundString(
-                            "Didn't have back to back slow speeds, checking for rapid ")
-                            .append(" deceleration on first pause only");
+                            "Didn't have back to back slow speeds, checking for rapid "
+                                    + " deceleration on first pause only");
                 }
                 if (mMakePauseHarderToTrigger) {
                     if (speed < mSpeedSlow) {
@@ -198,8 +195,8 @@
                         }
                         isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT;
                         isPausedReason = new ActiveGestureLog.CompoundString(
-                                "Maintained slow speed for sufficient duration when making")
-                                .append(" pause harder to trigger");
+                                "Maintained slow speed for sufficient duration when making"
+                                        + " pause harder to trigger");
                     } else {
                         mSlowStartTime = 0;
                         isPaused = false;
@@ -215,17 +212,14 @@
     private void updatePaused(boolean isPaused, ActiveGestureLog.CompoundString reason) {
         if (mDisallowPause) {
             reason = new ActiveGestureLog.CompoundString(
-                    "Disallow pause; otherwise, would have been ")
-                    .append(isPaused)
-                    .append(" due to reason:")
+                    "Disallow pause; otherwise, would have been %b due to reason: ", isPaused)
                     .append(reason);
             isPaused = false;
         }
         if (mIsPaused != isPaused) {
             mIsPaused = isPaused;
-            addLogs(new ActiveGestureLog.CompoundString("onMotionPauseChanged triggered; paused=")
-                    .append(mIsPaused)
-                    .append(", reason=")
+            addLogs(new ActiveGestureLog.CompoundString(
+                    "onMotionPauseChanged triggered; paused=%b, reason=", mIsPaused)
                     .append(reason));
             boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused;
             if (mIsPaused) {
@@ -245,14 +239,13 @@
         }
     }
 
-    private void addLogs(ActiveGestureLog.CompoundString compoundString) {
-        ActiveGestureLog.CompoundString logString =
-                new ActiveGestureLog.CompoundString("MotionPauseDetector: ")
-                        .append(compoundString);
+    private void addLogs(ActiveGestureLog.CompoundString event) {
         if (Utilities.isRunningInTestHarness()) {
-            Log.d(TAG, logString.toString());
+            Log.d(TAG, new ActiveGestureLog.CompoundString("MotionPauseDetector: ")
+                    .append(event)
+                    .toString());
         }
-        ActiveGestureLog.INSTANCE.addLog(logString);
+        ActiveGestureProtoLogProxy.logMotionPauseDetectorEvent(event);
     }
 
     public void clear() {
diff --git a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
index ffc345f..aa0fd88 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
@@ -28,9 +28,17 @@
  * 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)
+        }
 
     /**
-     * Sort task groups to move desktop tasks to the end of the list.
+     * 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.
@@ -40,6 +48,7 @@
         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()) {
@@ -49,31 +58,85 @@
         }
     }
 
-    /**
-     * Counts [TaskView]s that are [DesktopTaskView] instances.
-     *
-     * @param taskViews List of [TaskView]s
-     */
-    fun getDesktopTaskViewCount(taskViews: List<TaskView>): Int =
+    /** 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
      */
-    fun getFirstLargeTaskView(taskViews: List<TaskView>): TaskView? =
+    fun getFirstLargeTaskView(taskViews: Iterable<TaskView>): TaskView? =
         taskViews.firstOrNull { it.isLargeTile }
 
-    fun screenshotTasks(
-        taskView: TaskView,
-        recentsAnimationController: RecentsAnimationController,
-    ): Map<Int, ThumbnailData> =
-        taskView.taskContainers.associate {
-            it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id)
+    /** 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 the current list of [TaskView] children. */
-    fun getTaskViews(taskViewCount: Int, requireTaskViewAt: (Int) -> TaskView): List<TaskView> =
+    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..a94d023 100644
--- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
+++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
@@ -23,8 +23,10 @@
 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
@@ -44,9 +46,9 @@
  * 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
@@ -86,25 +88,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 +133,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 +154,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 +181,7 @@
                         1 / workspace.scaleX,
                         1 / workspace.scaleY,
                         transformed.centerX(),
-                        transformed.centerY()
+                        transformed.centerY(),
                     )
                 }
             )
@@ -183,6 +200,12 @@
                 Runnable {
                     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)
+                    }
                 }
             )
         )
@@ -193,6 +216,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 256e29e..3449cf2 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
@@ -225,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) primarySnapshotViewSize else secondarySnapshotViewSize
         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)
             )
@@ -262,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)
             )
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 1af12f1..60c6ade 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -35,7 +35,7 @@
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -52,7 +52,6 @@
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -116,7 +115,6 @@
     private static final String TAG = "SplitSelectStateCtor";
 
     private RecentsViewContainer mContainer;
-    private final Handler mHandler;
     private final RecentsModel mRecentTasksModel;
     @Nullable
     private Runnable mActivityBackCallback;
@@ -182,12 +180,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;
@@ -366,7 +363,7 @@
      * A version of {@link #launchSplitTasks(int, Consumer)} that launches with default split ratio.
      */
     public void launchSplitTasks(@Nullable Consumer<Boolean> callback) {
-        launchSplitTasks(SNAP_TO_50_50, callback);
+        launchSplitTasks(SNAP_TO_2_50_50, callback);
     }
 
     /**
@@ -374,7 +371,7 @@
      * ratio and no callback.
      */
     public void launchSplitTasks() {
-        launchSplitTasks(SNAP_TO_50_50, null);
+        launchSplitTasks(SNAP_TO_2_50_50, null);
     }
 
     /**
@@ -568,13 +565,13 @@
         switch (launchData.getSplitLaunchType()) {
             case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
                     optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
-                    SNAP_TO_50_50, remoteTransition, instanceId);
+                    SNAP_TO_2_50_50, remoteTransition, instanceId);
             case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
                     firstUserId, optionsBundle, secondTaskId, null /*options2*/,
-                    initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
+                    initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
             case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
                     initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
-                    initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
+                    initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
         }
     }
 
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/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/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 6db0923..8c854e7 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -59,7 +59,7 @@
             this,
             R.layout.task_thumbnail_deprecated,
             VIEW_POOL_MAX_SIZE,
-            VIEW_POOL_INITIAL_SIZE
+            VIEW_POOL_INITIAL_SIZE,
         )
     private val tempPointF = PointF()
     private val tempRect = Rect()
@@ -80,7 +80,7 @@
                             setTint(
                                 resources.getColor(
                                     android.R.color.system_neutral2_300,
-                                    context.theme
+                                    context.theme,
                                 )
                             )
                         }
@@ -92,8 +92,8 @@
                     ResourcesCompat.getDrawable(
                         context.resources,
                         R.drawable.ic_desktop_with_bg,
-                        context.theme
-                    )
+                        context.theme,
+                    ),
                 )
                 setText(resources.getText(R.string.recent_task_desktop))
             }
@@ -104,7 +104,7 @@
     fun bind(
         tasks: List<Task>,
         orientedState: RecentsOrientedState,
-        taskOverlayFactory: TaskOverlayFactory
+        taskOverlayFactory: TaskOverlayFactory,
     ) {
         if (DEBUG) {
             val sb = StringBuilder()
@@ -126,7 +126,7 @@
                     snapshotView,
                     // Add snapshotView to the front after initial views e.g. icon and
                     // background.
-                    childCountAtInflation
+                    childCountAtInflation,
                 )
                 TaskContainer(
                     this,
@@ -137,7 +137,7 @@
                     SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
                     digitalWellBeingToast = null,
                     showWindowsView = null,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 )
             }
         taskContainers.forEach { it.bind() }
@@ -159,12 +159,12 @@
     override fun updateTaskSize(
         lastComputedTaskSize: Rect,
         lastComputedGridTaskSize: Rect,
-        lastComputedCarouselTaskSize: Rect
+        lastComputedCarouselTaskSize: Rect,
     ) {
         super.updateTaskSize(
             lastComputedTaskSize,
             lastComputedGridTaskSize,
-            lastComputedCarouselTaskSize
+            lastComputedCarouselTaskSize,
         )
         if (taskContainers.isEmpty()) {
             return
@@ -186,7 +186,7 @@
             Log.d(
                 TAG,
                 "onMeasure: container=[$containerWidth,$containerHeight]" +
-                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]"
+                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]",
             )
         }
 
@@ -218,7 +218,7 @@
                     Log.d(
                         TAG,
                         "onMeasure: task=${it.task.key} size=[$width,$height]" +
-                            " margin=[$leftMargin,$topMargin]"
+                            " margin=[$leftMargin,$topMargin]",
                     )
                 }
             }
@@ -252,7 +252,7 @@
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "launchDesktopFromRecents",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val endCallback = RunnableList()
         val desktopController = recentsView.desktopRecentsController
@@ -262,7 +262,7 @@
         }
         Log.d(
             TAG,
-            "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated"
+            "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated",
         )
 
         // Callbacks get run from recentsView for case when recents animation already running
@@ -274,11 +274,13 @@
 
     override fun launchWithoutAnimation(
         isQuickSwitch: Boolean,
-        callback: (launched: Boolean) -> Unit
+        callback: (launched: Boolean) -> Unit,
     ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false)
 
-    // Desktop tile can't be in split screen
-    override fun confirmSecondSplitSelectApp(): Boolean = false
+    // Return true when Task cannot be launched as fullscreen (i.e. in split select state) to skip
+    // putting DesktopTaskView to split as it's not supported.
+    override fun confirmSecondSplitSelectApp(): Boolean =
+        recentsView?.canLaunchFullscreenTask() != true
 
     // TODO(b/330685808) support overlay for Screenshot action
     override fun setOverlayEnabled(overlayEnabled: Boolean) {}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
index 7b97c23..c07b7fb 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -59,10 +59,10 @@
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
-    defStyleRes: Int = 0
+    defStyleRes: Int = 0,
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-    private val recentsViewContainer =
-        RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
+    private val recentsViewContainer: RecentsViewContainer =
+        RecentsViewContainer.containerFromContext(context)
 
     private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java)
 
@@ -138,7 +138,7 @@
                 usageLimit =
                     launcherApps?.getAppUsageLimit(
                         task.topComponent.packageName,
-                        UserHandle.of(task.key.userId)
+                        UserHandle.of(task.key.userId),
                     )
             } catch (e: Exception) {
                 Log.e(TAG, "Error initializing digital well being toast", e)
@@ -162,7 +162,7 @@
         task: Task,
         taskView: TaskView,
         snapshotView: View,
-        @StagePosition stagePosition: Int
+        @StagePosition stagePosition: Int,
     ) {
         this.task = task
         this.taskView = taskView
@@ -201,7 +201,7 @@
 
     private fun getReadableDuration(
         duration: Duration,
-        @StringRes durationLessThanOneMinuteStringId: Int
+        @StringRes durationLessThanOneMinuteStringId: Int,
     ): String {
         val hours = Math.toIntExact(duration.toHours())
         val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes())
@@ -211,7 +211,7 @@
                 MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW)
                     .formatMeasures(
                         Measure(hours, MeasureUnit.HOUR),
-                        Measure(minutes, MeasureUnit.MINUTE)
+                        Measure(minutes, MeasureUnit.MINUTE),
                     )
             // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
             hours > 0 ->
@@ -239,7 +239,7 @@
     @VisibleForTesting
     fun getBannerText(
         remainingTime: Long = appRemainingTimeMs,
-        forContentDesc: Boolean = false
+        forContentDesc: Boolean = false,
     ): String {
         val duration =
             Duration.ofMillis(
@@ -250,7 +250,7 @@
         val readableDuration =
             getReadableDuration(
                 duration,
-                R.string.shorter_duration_less_than_one_minute /* forceFormatWidth */
+                R.string.shorter_duration_less_than_one_minute, /* forceFormatWidth */
             )
         val splitBannerConfig = getSplitBannerConfig()
         return when {
@@ -277,7 +277,7 @@
             Log.e(
                 TAG,
                 "Failed to open app usage settings for task " + task.topComponent.packageName,
-                e
+                e,
             )
         }
     }
@@ -285,13 +285,13 @@
     private fun getContentDescriptionForTask(
         task: Task,
         appUsageLimitTimeMs: Long,
-        appRemainingTimeMs: Long
+        appRemainingTimeMs: Long,
     ): String? =
         if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0)
             context.getString(
                 R.string.task_contents_description_with_remaining_time,
                 task.titleDescription,
-                getBannerText(appRemainingTimeMs, true /* forContentDesc */)
+                getBannerText(appRemainingTimeMs, true /* forContentDesc */),
             )
         else task.titleDescription
 
@@ -310,7 +310,7 @@
                     recentsViewContainer.deviceProfile,
                     splitBounds,
                     taskView.layoutParams.width,
-                    taskView.layoutParams.height
+                    taskView.layoutParams.height,
                 )
             if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
                 snapshotWidth = groupedTaskSize.first.x
@@ -327,7 +327,7 @@
             recentsViewContainer.deviceProfile,
             snapshotWidth,
             snapshotHeight,
-            this
+            this,
         )
     }
 
@@ -340,7 +340,7 @@
                 recentsViewContainer.deviceProfile,
                 taskView.snapshotViews,
                 task.key.id,
-                this
+                this,
             )
         this.translationX = translationX
         this.splitOffsetTranslationY = translationY
@@ -372,7 +372,7 @@
             if (taskView.containsMultipleTasks())
                 context.getString(
                     R.string.split_app_usage_settings,
-                    TaskUtils.getTitle(context, task)
+                    TaskUtils.getTitle(context, task),
                 )
             else context.getString(R.string.accessibility_app_usage_settings)
         return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label)
@@ -394,7 +394,7 @@
             /** Used for grid task view, only showing icon and time */
             SPLIT_GRID_BANNER_LARGE,
             /** Used for grid task view, only showing icon */
-            SPLIT_GRID_BANNER_SMALL
+            SPLIT_GRID_BANNER_SMALL,
         }
 
         val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS)
diff --git a/quickstep/src/com/android/quickstep/views/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/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 3fd1a6b..92c1e93 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -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()
         }
@@ -173,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(
@@ -191,7 +180,8 @@
                 layoutParams.width,
                 isRtl,
                 container.deviceProfile,
-                splitBoundsConfig
+                splitBoundsConfig,
+                inSplitSelection
             )
         } else {
             pagedOrientationHandler.setSplitIconParams(
@@ -204,7 +194,8 @@
                 measuredWidth,
                 isRtl,
                 container.deviceProfile,
-                splitBoundsConfig
+                splitBoundsConfig,
+                inSplitSelection
             )
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index d20d0a5..00e57c2 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.flags.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.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -173,8 +170,7 @@
 
     @Override
     public void onStateTransitionComplete(LauncherState finalState) {
-        if (finalState == NORMAL || finalState == SPRING_LOADED  || finalState == EDIT_MODE
-                || finalState == ALL_APPS) {
+        if (!finalState.isRecentsViewVisible) {
             // Clean-up logic that occurs when recents is no longer in use/visible.
             reset();
         }
@@ -254,7 +250,7 @@
     }
 
     @Override
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         if (FeatureFlags.enableSplitContextually()) {
             return !mSplitSelectStateController.isSplitSelectActive();
         } else {
@@ -268,7 +264,8 @@
         super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
         DesktopVisibilityController desktopVisibilityController =
                 mContainer.getDesktopVisibilityController();
-        if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && desktopVisibilityController != null) {
+        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+                && desktopVisibilityController != null) {
             // TODO: b/333533253 - Remove after flag rollout
             desktopVisibilityController.setRecentsGestureStart();
         }
@@ -291,7 +288,8 @@
             }
         }
         super.onGestureAnimationEnd();
-        if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && 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/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index f8a0f45..a075c58 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -205,8 +205,7 @@
 import com.android.quickstep.recents.di.RecentsDependencies;
 import com.android.quickstep.recents.viewmodel.RecentsViewData;
 import com.android.quickstep.recents.viewmodel.RecentsViewModel;
-import com.android.quickstep.util.ActiveGestureErrorDetector;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
@@ -258,7 +257,7 @@
  * @param <STATE_TYPE>     : the type of base state that will be used
  */
 public abstract class RecentsView<
-        CONTAINER_TYPE extends Context & RecentsViewContainer,
+        CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
         TaskVisualsChangeListener {
@@ -325,25 +324,18 @@
             new FloatProperty<RecentsView>("runningTaskAttachAlpha") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
-                    TaskView runningTask = recentsView.getRunningTaskView();
-                    if (runningTask == null) {
-                        return;
-                    }
-                    runningTask.setAttachAlpha(v);
+                    recentsView.mRunningTaskAttachAlpha = v;
+                    recentsView.applyAttachAlpha();
                 }
 
                 @Override
                 public Float get(RecentsView recentsView) {
-                    TaskView runningTask = recentsView.getRunningTaskView();
-                    if (runningTask == null) {
-                        return null;
-                    }
-                    return runningTask.getAttachAlpha();
+                    return recentsView.mRunningTaskAttachAlpha;
                 }
             };
 
     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;
@@ -464,6 +456,21 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> DESKTOP_CAROUSEL_DETACH_PROGRESS =
+            new FloatProperty<>("desktopCarouselDetachProgress") {
+                @Override
+                public void setValue(RecentsView view, float offset) {
+                    view.mDesktopCarouselDetachProgress = offset;
+                    view.applyAttachAlpha();
+                    view.updatePageOffsets();
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.mDesktopCarouselDetachProgress;
+                }
+            };
+
     /**
      * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and
      * being in any other state has a value of 0.
@@ -575,6 +582,7 @@
     private int mClampedScrollOffsetBound;
 
     private float mAdjacentPageHorizontalOffset = 0;
+    private float mDesktopCarouselDetachProgress = 0;
     protected float mTaskViewsSecondaryTranslation = 0;
     protected float mTaskViewsPrimarySplitTranslation = 0;
     protected float mTaskViewsSecondarySplitTranslation = 0;
@@ -687,6 +695,7 @@
 
     private boolean mTaskIconScaledDown = false;
     private boolean mRunningTaskShowScreenshot = false;
+    private float mRunningTaskAttachAlpha;
 
     private boolean mOverviewStateEnabled;
     private boolean mHandleTaskStackChanges;
@@ -798,12 +807,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
@@ -1177,7 +1180,7 @@
      *
      * @return {@code true} if child TaskViews can be launched when user taps on them
      */
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         return true;
     }
 
@@ -1208,6 +1211,7 @@
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
+
         updateTaskStackListenerState();
         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
         mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
@@ -1585,8 +1589,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);
         }
@@ -1790,8 +1793,7 @@
 
     @Override
     protected void onScrollerAnimationAborted() {
-        ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
-                ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+        ActiveGestureProtoLogProxy.logOnScrollerAnimationAborted();
     }
 
     @Override
@@ -1871,7 +1873,6 @@
         mFilterState.updateInstanceCountMap(taskGroups);
 
         // Clear out desktop view if it is set
-        mDesktopTaskView = null;
 
         // Move Desktop Tasks to the end of the list
         if (enableLargeDesktopWindowingTile()) {
@@ -1910,7 +1911,6 @@
                                 .toList();
                 ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState,
                         mTaskOverlayFactory);
-                mDesktopTaskView = (DesktopTaskView) taskView;
             } else {
                 Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
                         : groupTask.task1;
@@ -2094,14 +2094,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
@@ -2650,10 +2646,6 @@
         return getTaskViewFromTaskViewId(mFocusedTaskViewId);
     }
 
-    private @Nullable TaskView getFirstLargeTaskView() {
-        return mUtils.getFirstLargeTaskView(getTaskViews());
-    }
-
     @Nullable
     private TaskView getTaskViewFromTaskViewId(int taskViewId) {
         if (taskViewId == -1) {
@@ -2749,7 +2741,7 @@
         showCurrentTask(mActiveGestureRunningTasks);
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
-        setRunningTaskHidden(!shouldUpdateRunningTaskAlpha());
+        setRunningTaskHidden(true);
         setTaskIconScaledDown(true);
     }
 
@@ -2873,6 +2865,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);
+            }
+        }
     }
 
     /**
@@ -3039,17 +3039,27 @@
      */
     public void setRunningTaskHidden(boolean isHidden) {
         mRunningTaskTileHidden = isHidden;
+        // mRunningTaskAttachAlpha can be changed by RUNNING_TASK_ATTACH_ALPHA animation without
+        // changing mRunningTaskTileHidden.
+        mRunningTaskAttachAlpha = isHidden ? 0f : 1f;
         TaskView runningTask = getRunningTaskView();
         if (runningTask == null) {
             return;
         }
-        runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
+        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);
     }
@@ -3658,8 +3668,8 @@
             float longGridRowWidthDiff = 0;
 
             int topGridRowSize = mTopRowIdSet.size();
-            int bottomGridRowSize = taskCount - mTopRowIdSet.size()
-                    - (enableGridOnlyOverview() ? 0 : 1);
+            int numLargeTiles = mUtils.getLargeTileCount(getTaskViews());
+            int bottomGridRowSize = taskCount - mTopRowIdSet.size() - numLargeTiles;
             boolean topRowLonger = topGridRowSize > bottomGridRowSize;
             boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
             boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
@@ -3791,74 +3801,15 @@
                     }
                 }
             } 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;
-                    }
-                }
-
+                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) {
@@ -3958,12 +3909,12 @@
         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) {
                     finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
-                            () -> onEnd(success));
+                            () -> onEnd(true));
                 } else {
                     onEnd(success);
                 }
@@ -4133,6 +4084,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.
@@ -4166,6 +4125,87 @@
     }
 
     /**
+     * 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) {
+        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;
+            }
+        }
+        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
@@ -4328,9 +4368,8 @@
         }
 
         // Init task grid nav helper with top/bottom id arrays.
-        // TODO(b/361070854): Add keyboard navigation for all large tiles.
         TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
-                getBottomRowIdArray(), mFocusedTaskViewId);
+                getBottomRowIdArray(), mUtils.getLargeTaskViewIds(getTaskViews()));
 
         // Get current page's task view ID.
         TaskView currentPageTaskView = getCurrentPageTaskView();
@@ -4448,11 +4487,7 @@
         alpha = Utilities.boundToRange(alpha, 0, 1);
         mContentAlpha = alpha;
 
-        TaskView runningTaskView = getRunningTaskView();
         for (TaskView taskView : getTaskViews()) {
-            if (runningTaskView != null && mRunningTaskTileHidden && taskView == runningTaskView) {
-                continue;
-            }
             taskView.setStableAlpha(alpha);
         }
         mClearAllButton.setContentAlpha(mContentAlpha);
@@ -4565,7 +4600,7 @@
     /**
      * Returns the current list of [TaskView] children.
      */
-    private List<TaskView> getTaskViews() {
+    private Iterable<TaskView> getTaskViews() {
         return mUtils.getTaskViews(getTaskViewCount(), this::requireTaskViewAt);
     }
 
@@ -4665,6 +4700,10 @@
                 ? (runningTask == null ? INVALID_PAGE : indexOfChild(runningTask))
                 : mOffsetMidpointIndexOverride;
         int modalMidpoint = getCurrentPage();
+        TaskView carouselHiddenMidpointTask = runningTask != null ? runningTask
+                : mUtils.getFirstTaskViewInCarousel(/*nonRunningTaskCarouselHidden=*/true,
+                        getTaskViews(), null);
+        int carouselHiddenMidpoint = indexOfChild(carouselHiddenMidpointTask);
         boolean shouldCalculateOffsetForAllTasks = showAsGrid
                 && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile())
                 && mTaskModalness > 0;
@@ -4684,6 +4723,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
@@ -4701,7 +4741,10 @@
                     : 0;
         }
 
+        int primarySize = getPagedOrientationHandler().getPrimaryValue(getWidth(), getHeight());
+        float maxOverscroll = primarySize * OverScroll.OVERSCROLL_DAMP_FACTOR;
         for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
             float translation = i == midpoint
                     ? midpointOffsetSize
                     : i < midpoint
@@ -4711,16 +4754,31 @@
                 gridOffsetSize = getHorizontalOffsetSize(i, modalMidpoint, modalOffset);
                 gridOffsetSize = Math.abs(gridOffsetSize) * (i <= modalMidpoint ? 1 : -1);
             }
+            if (enableLargeDesktopWindowingTile()) {
+                if (child instanceof TaskView
+                        && !mUtils.isVisibleInCarousel((TaskView) child,
+                        runningTask, /*nonRunningTaskCarouselHidden=*/true)) {
+                    // Increment carouselHiddenOffsetSize by maxOverscroll so it won't be on screen
+                    // even when user overscroll.
+                    carouselHiddenOffsetSize = (Math.abs(getMaxHorizontalOffsetSize(i,
+                            carouselHiddenMidpoint)) + maxOverscroll)
+                            * mDesktopCarouselDetachProgress;
+                    carouselHiddenOffsetSize = carouselHiddenOffsetSize * (
+                            i <= carouselHiddenMidpoint ? 1 : -1);
+                } else {
+                    carouselHiddenOffsetSize = 0;
+                }
+            }
             float modalTranslation = i == modalMidpoint
                     ? modalMidpointOffsetSize
                     : showAsGrid
                             ? gridOffsetSize
                             : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize;
-            View child = getChildAt(i);
             boolean skipTranslationOffset = enableDesktopTaskAlphaAnimation()
                     && i == getRunningTaskIndex()
                     && child instanceof DesktopTaskView;
-            float totalTranslationX = (skipTranslationOffset ? 0f : translation) + modalTranslation;
+            float totalTranslationX = (skipTranslationOffset ? 0f : translation) + modalTranslation
+                    + carouselHiddenOffsetSize;
             FloatProperty translationPropertyX = child instanceof TaskView
                     ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
                     : getPagedOrientationHandler().getPrimaryViewTranslate();
@@ -4777,6 +4835,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;
@@ -4836,7 +4902,7 @@
             }
             distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
         }
-        return distanceToOffscreen * offsetProgress;
+        return distanceToOffscreen;
     }
 
     /**
@@ -4934,7 +5000,6 @@
         mSplitSelectStateController.setAnimateCurrentTaskDismissal(
                 true /*animateCurrentTaskDismissal*/);
         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
-        updateDesktopTaskVisibility(false /* visible */);
     }
 
     /**
@@ -4956,13 +5021,6 @@
         mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
                 splitSelectSource.position.stagePosition, splitSelectSource.getItemInfo(),
                 splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
-        updateDesktopTaskVisibility(false /* visible */);
-    }
-
-    private void updateDesktopTaskVisibility(boolean visible) {
-        if (mDesktopTaskView != null) {
-            mDesktopTaskView.setVisibility(visible ? VISIBLE : GONE);
-        }
     }
 
     /**
@@ -4986,7 +5044,8 @@
             mSplitSelectStateController.getSplitAnimationController()
                     .addInitialSplitFromPair(taskContainer, builder,
                             mContainer.getDeviceProfile(),
-                            mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
+                            mSplitHiddenTaskView.getLayoutParams().width,
+                            mSplitHiddenTaskView.getLayoutParams().height,
                             primaryTaskSelected);
             builder.addOnFrameCallback(() -> {
                 if (!enableRefactorTaskThumbnail()) {
@@ -5171,7 +5230,6 @@
             mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
             mSplitHiddenTaskView = null;
         }
-        updateDesktopTaskVisibility(true /* visible */);
     }
 
     private void safeRemoveDragLayerView(@Nullable View viewToRemove) {
@@ -5795,26 +5853,44 @@
     }
 
     private int getFirstViewIndex() {
-        TaskView firstTaskView = mShowAsGridLastOnLayout ? getFirstLargeTaskView() : null;
-        return firstTaskView != null ? indexOfChild(firstTaskView) : 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());
+            if (firstLargeTaskView != null) {
+                firstView = firstLargeTaskView;
+            } else {
+                firstView = getTaskViewAt(0);
+            }
+        } 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(getFirstLargeTaskView());
+        return indexOfChild(lastView);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index 060c71e..d8036aa 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -17,6 +17,7 @@
 package com.android.quickstep.views;
 
 import android.app.Activity;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.LocusId;
@@ -26,9 +27,11 @@
 import android.view.View;
 import android.view.Window;
 
+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.views.ActivityContext;
 import com.android.launcher3.views.ScrimView;
 
@@ -41,7 +44,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 +55,6 @@
     }
 
     /**
-     * Returns {@link SystemUiController} to manage various window flags to control system UI.
-     */
-    SystemUiController getSystemUiController();
-
-    /**
      * Returns {@link ScrimView}
      */
     ScrimView getScrimView();
@@ -92,7 +90,7 @@
     /**
      * Returns overview actions view as a view
      */
-    View getActionsView();
+    OverviewActionsView getActionsView();
 
     /**
      * @see BaseActivity#addForceInvisibleFlag(int)
@@ -140,12 +138,6 @@
     void runOnBindToTouchInteractionService(Runnable r);
 
     /**
-     * @see Activity#getWindow()
-     * @return Window
-     */
-    Window getWindow();
-
-    /**
      * @see
      * BaseActivity#addMultiWindowModeChangedListener(BaseActivity.MultiWindowModeChangedListener)
      * @param listener {@link BaseActivity.MultiWindowModeChangedListener}
@@ -174,6 +166,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 +209,7 @@
                                         .setOrientationHandler(orientationForLogging))
                         .build());
     }
+
+    @Nullable
+    DesktopVisibilityController getDesktopVisibilityController();
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index 4604b70..f22c672 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -49,9 +49,7 @@
         recentsViewModel.setRunningTaskShowScreenshot(true)
         viewAttachedScope.launch {
             recentsViewModel.waitForRunningTaskShowScreenshotToUpdate()
-            if (updatedThumbnails != null) {
-                recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails)
-            }
+            recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails)
             ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 13c4f78..6cb7741 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -56,7 +56,7 @@
     @SplitConfigurationOptions.StagePosition val stagePosition: Int,
     val digitalWellBeingToast: DigitalWellBeingToast?,
     val showWindowsView: View?,
-    taskOverlayFactory: TaskOverlayFactory
+    taskOverlayFactory: TaskOverlayFactory,
 ) {
     val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
     lateinit var taskContainerData: TaskContainerData
@@ -160,6 +160,8 @@
         if (enableRefactorTaskThumbnail()) {
             taskView.removeView(thumbnailView)
         }
+        snapshotView.scaleX = 1f
+        snapshotView.scaleY = 1f
         overlay.destroy()
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 601dae8..f513a82 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -46,7 +46,6 @@
 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
@@ -55,7 +54,6 @@
 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
@@ -84,7 +82,6 @@
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
-import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.task.viewmodel.TaskViewModel
 import com.android.quickstep.util.ActiveGestureErrorDetector
 import com.android.quickstep.util.ActiveGestureLog
@@ -201,14 +198,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>
@@ -223,21 +220,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)
@@ -399,7 +396,7 @@
         }
         get() = taskViewAlpha.get(ALPHA_INDEX_STABLE).value
 
-    protected var attachAlpha
+    var attachAlpha
         set(value) {
             taskViewAlpha.get(ALPHA_INDEX_ATTACH).value = value
         }
@@ -435,7 +432,7 @@
             field = value
             Log.d(
                 TAG,
-                "${taskIds.contentToString()} - setting border animator visibility to: $field"
+                "${taskIds.contentToString()} - setting border animator visibility to: $field",
             )
             hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true)
         }
@@ -455,7 +452,7 @@
             FOCUS_TRANSITION,
             FOCUS_TRANSITION_INDEX_COUNT,
             { x: Float, y: Float -> x * y },
-            1f
+            1f,
         )
     private val focusTransitionFullscreen =
         focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN)
@@ -486,27 +483,23 @@
             taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type)
         }
 
-        val keyboardFocusHighlightEnabled =
-            (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline())
         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)
@@ -519,8 +512,8 @@
                             this,
                             it.getColor(
                                 R.styleable.TaskView_hoverBorderColor,
-                                BorderAnimator.DEFAULT_BORDER_COLOR
-                            )
+                                BorderAnimator.DEFAULT_BORDER_COLOR,
+                            ),
                         )
                     else null
         }
@@ -612,6 +605,7 @@
 
     override fun onRecycle() {
         resetPersistentViewTransforms()
+        attachAlpha = 1f
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
         if (!enableRefactorTaskThumbnail()) {
@@ -634,7 +628,7 @@
             addAction(
                 AccessibilityAction(
                     R.id.action_close,
-                    context.getText(R.string.accessibility_close)
+                    context.getText(R.string.accessibility_close),
                 )
             )
 
@@ -658,7 +652,7 @@
                         1,
                         it.taskViewCount - it.indexOfChild(this@TaskView) - 1,
                         1,
-                        false
+                        false,
                     )
             }
         }
@@ -693,7 +687,6 @@
         orientedState: RecentsOrientedState,
         taskOverlayFactory: TaskOverlayFactory,
     ) {
-
         cancelPendingLoadTasks()
         taskContainers =
             listOf(
@@ -704,7 +697,7 @@
                     R.id.show_windows,
                     R.id.digital_wellbeing_toast,
                     STAGE_POSITION_UNDEFINED,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 )
             )
         taskContainers.forEach { it.bind() }
@@ -726,6 +719,7 @@
                 thumbnailViewDeprecated.visibility = GONE
                 val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated)
                 LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false).also {
+                    it.id = thumbnailViewId
                     addView(it, indexOfSnapshotView, thumbnailViewDeprecated.layoutParams)
                 }
             } else {
@@ -742,7 +736,7 @@
             stagePosition,
             digitalWellBeingToast,
             findViewById(showWindowViewId)!!,
-            taskOverlayFactory
+            taskOverlayFactory,
         )
     }
 
@@ -860,7 +854,7 @@
             if (relativeToDragLayer) {
                 container.dragLayer.getDescendantRectRelativeToSelf(
                     it.snapshotView,
-                    thumbnailBounds
+                    thumbnailBounds,
                 )
             } else {
                 thumbnailBounds.set(it.snapshotView)
@@ -979,7 +973,7 @@
     @JvmOverloads
     open fun setShouldShowScreenshot(
         shouldShowScreenshot: Boolean,
-        thumbnailDatas: Map<Int, ThumbnailData?>? = null
+        thumbnailDatas: Map<Int, ThumbnailData?>? = null,
     ) {
         if (this.shouldShowScreenshot == shouldShowScreenshot) return
         this.shouldShowScreenshot = shouldShowScreenshot
@@ -1030,7 +1024,7 @@
         if (!isClickableAsLiveTile) {
             Log.e(
                 TAG,
-                "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}"
+                "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}",
             )
             return null
         }
@@ -1049,7 +1043,7 @@
                     apps.toTypedArray(),
                     wallpapers.toTypedArray(),
                     remoteTargetHandles[0].transformParams.targetSet.nonApps,
-                    remoteTargetHandles[0].transformParams.targetSet.targetMode
+                    remoteTargetHandles[0].transformParams.targetSet.targetMode,
                 )
             }
         if (targets == null) {
@@ -1059,7 +1053,7 @@
             if (runnableList == null) {
                 Log.e(
                     TAG,
-                    "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}"
+                    "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}",
                 )
             }
             isClickableAsLiveTile = true
@@ -1068,7 +1062,7 @@
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "composeRecentsLaunchAnimator",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val runnableList = RunnableList()
         with(AnimatorSet()) {
@@ -1081,7 +1075,7 @@
                 true /* launcherClosing */,
                 recentsView.stateManager,
                 recentsView,
-                recentsView.depthController
+                recentsView.depthController,
             )
             addListener(
                 object : AnimatorListenerAdapter() {
@@ -1118,7 +1112,7 @@
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "startActivityFromRecentsAsync",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val opts =
             container.getActivityLaunchOptions(this, null).apply {
@@ -1130,7 +1124,7 @@
         ) {
             Log.d(
                 TAG,
-                "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}"
+                "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}",
             )
             ActiveGestureLog.INSTANCE.trackEvent(
                 ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED
@@ -1163,12 +1157,12 @@
     @JvmOverloads
     open fun launchWithoutAnimation(
         isQuickSwitch: Boolean = false,
-        callback: (launched: Boolean) -> Unit
+        callback: (launched: Boolean) -> Unit,
     ) {
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "startActivityFromRecentsAsync",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val firstContainer = taskContainers[0]
         val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext)
@@ -1199,7 +1193,7 @@
                     0,
                     0,
                     Executors.MAIN_EXECUTOR.handler,
-                    { callback(true) }
+                    { callback(true) },
                 ) {
                     failureListener.onTransitionFinished()
                 }
@@ -1227,7 +1221,7 @@
             }
             Log.d(
                 TAG,
-                "launchWithoutAnimation - startActivityFromRecents: ${taskIds.contentToString()}"
+                "launchWithoutAnimation - startActivityFromRecents: ${taskIds.contentToString()}",
             )
         }
     }
@@ -1246,7 +1240,7 @@
         recentsView?.initiateSplitSelect(
             this,
             splitPositionOption.stagePosition,
-            SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition)
+            SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition),
         )
     }
 
@@ -1269,7 +1263,7 @@
             container.splitAnimationThumbnail,
             /* intent */ null,
             /* user */ null,
-            container.itemInfo
+            container.itemInfo,
         )
     }
 
@@ -1374,13 +1368,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(),
         )
     }
 
@@ -1390,7 +1384,7 @@
             it.showWindowsView?.let { showWindowsView ->
                 updateFilterCallback(
                     showWindowsView,
-                    getFilterUpdateCallback(it.task.key.packageName)
+                    getFilterUpdateCallback(it.task.key.packageName),
                 )
             }
         }
@@ -1593,10 +1587,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
@@ -1612,7 +1603,6 @@
         }
         dismissScale = 1f
         translationZ = 0f
-        attachAlpha = 1f
         setIconScaleAndDim(1f)
         setColorTint(0f, 0)
     }
@@ -1710,7 +1700,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/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java
similarity index 99%
rename from quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 2398e66..ab10979 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java
similarity index 78%
rename from quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java
index d46b8fc..23e245c 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,11 +18,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.util.Preconditions;
-
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
@@ -72,14 +71,6 @@
         addLog(event, null);
     }
 
-    public void addLog(@NonNull String event, int extras) {
-        addLog(event, extras, null);
-    }
-
-    public void addLog(@NonNull String event, boolean extras) {
-        addLog(event, extras, null);
-    }
-
     /**
      * Adds a log to be printed at log-dump-time and track the associated event for error detection.
      *
@@ -90,20 +81,6 @@
         addLog(new CompoundString(event), gestureEvent);
     }
 
-    public void addLog(
-            @NonNull String event,
-            int extras,
-            @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
-        addLog(new CompoundString(event).append(": ").append(extras), gestureEvent);
-    }
-
-    public void addLog(
-            @NonNull String event,
-            boolean extras,
-            @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
-        addLog(new CompoundString(event).append(": ").append(extras), gestureEvent);
-    }
-
     public void addLog(@NonNull CompoundString compoundString) {
         addLog(compoundString, null);
     }
@@ -252,25 +229,27 @@
     /** A buildable string stored as an array for memory efficiency. */
     public static class CompoundString {
 
-        public static final CompoundString NO_OP = new CompoundString();
+        public static final CompoundString NO_OP = new CompoundString(true);
 
         private final List<String> mSubstrings;
         private final List<Object> mArgs;
 
         private final boolean mIsNoOp;
 
-        private CompoundString() {
-            this(null);
+        public static CompoundString newEmptyString() {
+            return new CompoundString(false);
         }
 
-        public CompoundString(String substring) {
-            mIsNoOp = substring == null;
+        private CompoundString(boolean isNoOp) {
+            mIsNoOp = isNoOp;
             mSubstrings = mIsNoOp ? null : new ArrayList<>();
             mArgs = mIsNoOp ? null : new ArrayList<>();
+        }
 
-            if (!mIsNoOp) {
-                mSubstrings.add(substring);
-            }
+        public CompoundString(String substring, Object... args) {
+            this(substring == null);
+
+            append(substring, args);
         }
 
         public CompoundString append(CompoundString substring) {
@@ -283,80 +262,24 @@
             return this;
         }
 
-        public CompoundString append(String substring) {
+        public CompoundString append(String substring, Object... args) {
             if (mIsNoOp) {
                 return this;
             }
             mSubstrings.add(substring);
+            mArgs.addAll(Arrays.stream(args).toList());
 
             return this;
         }
 
-        public CompoundString append(int num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%d");
-        }
-
-        public CompoundString append(long num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%d");
-        }
-
-        public CompoundString append(float num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%.2f");
-        }
-
-        public CompoundString append(double num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%.2f");
-        }
-
-        public CompoundString append(boolean bool) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(bool);
-
-            return append("%b");
-        }
-
-        private Object[] getArgs() {
-            Preconditions.assertTrue(!mIsNoOp);
-
-            return mArgs.toArray();
-        }
-
         @Override
         public String toString() {
-            return String.format(toUnformattedString(), getArgs());
-        }
-
-        private String toUnformattedString() {
-            Preconditions.assertTrue(!mIsNoOp);
-
+            if (mIsNoOp) return null;
             StringBuilder sb = new StringBuilder();
             for (String substring : mSubstrings) {
                 sb.append(substring);
             }
-
-            return sb.toString();
+            return String.format(sb.toString(), mArgs.toArray());
         }
 
         @Override
@@ -366,10 +289,9 @@
 
         @Override
         public boolean equals(Object obj) {
-            if (!(obj instanceof CompoundString)) {
+            if (!(obj instanceof CompoundString other)) {
                 return false;
             }
-            CompoundString other = (CompoundString) obj;
             return (mIsNoOp == other.mIsNoOp)
                     && Objects.equals(mSubstrings, other.mSubstrings)
                     && Objects.equals(mArgs, other.mArgs);
diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
new file mode 100644
index 0000000..f43a125
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.MotionEvent.ACTION_DOWN;
+
+import static com.android.launcher3.Flags.enableActiveGestureProtoLog;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_CANCEL_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_START_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
+import static com.android.quickstep.util.QuickstepProtoLogGroup.ACTIVE_GESTURE_LOG;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+/**
+ * Proxy class used for ActiveGestureLog ProtoLog support.
+ * <p>
+ * This file will have all of its static strings in the
+ * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings.
+ * <p>
+ * When a new ActiveGestureLog entry needs to be added to the codebase (or and existing entry needs
+ * to be modified), add it here under a new unique method and make sure the ProtoLog entry matches
+ * to avoid confusion.
+ */
+public class ActiveGestureProtoLogProxy {
+
+    public static void logLauncherDestroyed() {
+        ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed");
+    }
+
+    public static void logAbsSwipeUpHandlerOnRecentsAnimationCanceled() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "AbsSwipeUpHandler.onRecentsAnimationCanceled",
+                /* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onRecentsAnimationCanceled");
+    }
+
+    public static void logAbsSwipeUpHandlerOnRecentsAnimationFinished() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "RecentsAnimationCallbacks.onAnimationFinished",
+                ON_FINISH_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onAnimationFinished");
+    }
+
+    public static void logAbsSwipeUpHandlerCancelCurrentAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(
+                "AbsSwipeUpHandler.cancelCurrentAnimation",
+                ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.cancelCurrentAnimation");
+    }
+
+    public static void logAbsSwipeUpHandlerOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.onTasksAppeared: "
+                + "force finish recents animation complete; clearing state callback.");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onTasksAppeared: "
+                + "force finish recents animation complete; clearing state callback.");
+    }
+
+    public static void logFinishRecentsAnimationOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimationOnTasksAppeared");
+    }
+
+    public static void logRecentsAnimationCallbacksOnAnimationCancelled() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled",
+                /* gestureEvent= */ ON_CANCEL_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationCanceled");
+    }
+
+    public static void logRecentsAnimationCallbacksOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared",
+                ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onTasksAppeared");
+    }
+
+    public static void logStartRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "TaskAnimationManager.startRecentsAnimation",
+                /* gestureEvent= */ START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TaskAnimationManager.startRecentsAnimation");
+    }
+
+    public static void logLaunchingSideTaskFailed() {
+        ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Unable to launch side task (no recents)");
+    }
+
+    public static void logContinueRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "continueRecentsAnimation");
+    }
+
+    public static void logCleanUpRecentsAnimationSkipped() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation skipped due to wrong callbacks");
+    }
+
+    public static void logCleanUpRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation");
+    }
+
+    public static void logOnInputEventUserLocked() {
+        ActiveGestureLog.INSTANCE.addLog(
+                "TIS.onInputEvent: Cannot process input event: user is locked");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Cannot process input event: user is locked");
+    }
+
+    public static void logOnInputIgnoringFollowingEvents() {
+        ActiveGestureLog.INSTANCE.addLog("TIS.onMotionEvent: A new gesture has been started, "
+                        + "but a previously-requested recents animation hasn't started. "
+                        + "Ignoring all following motion events.",
+                RECENTS_ANIMATION_START_PENDING);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: A new gesture has been started, "
+                + "but a previously-requested recents animation hasn't started. "
+                + "Ignoring all following motion events.");
+    }
+
+    public static void logOnInputEventThreeButtonNav() {
+        ActiveGestureLog.INSTANCE.addLog("TIS.onInputEvent: Cannot process input event: "
+                + "using 3-button nav and event is not a trackpad event");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onInputEvent: Cannot process input event: "
+                + "using 3-button nav and event is not a trackpad event");
+    }
+
+    public static void logPreloadRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog("preloadRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "preloadRecentsAnimation");
+    }
+
+    public static void logRecentTasksMissing() {
+        ActiveGestureLog.INSTANCE.addLog("Null mRecentTasks", RECENT_TASKS_MISSING);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Null mRecentTasks");
+    }
+
+    public static void logExecuteHomeCommand() {
+        ActiveGestureLog.INSTANCE.addLog("OverviewCommandHelper.executeCommand(HOME)");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewCommandHelper.executeCommand(HOME)");
+    }
+
+    public static void logFinishRecentsAnimationCallback() {
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation-callback");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation-callback");
+    }
+
+    public static void logOnScrollerAnimationAborted() {
+        ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
+                ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "scroller animation aborted");
+    }
+
+    public static void logInputConsumerBecameActive(@NonNull String consumerName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "%s became active", consumerName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "%s became active", consumerName);
+    }
+
+    public static void logTaskLaunchFailed(int launchedTaskId) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launch failed, task (id=%d) finished mid transition", launchedTaskId));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Launch failed, task (id=%d) finished mid transition", launchedTaskId);
+    }
+
+    public static void logOnPageEndTransition(int nextPageIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onPageEndTransition: current page index updated: %d", nextPageIndex));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onPageEndTransition: current page index updated: %d", nextPageIndex);
+    }
+
+    public static void logQuickSwitchFromHomeFallback(int taskIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Quick switch from home fallback case: The TaskView at index %d is missing.",
+                        taskIndex),
+                QUICK_SWITCH_FROM_HOME_FALLBACK);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Quick switch from home fallback case: The TaskView at index %d is missing.",
+                taskIndex);
+    }
+
+    public static void logQuickSwitchFromHomeFailed(int taskIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Quick switch from home failed: TaskViews at indices %d and 0 are missing.",
+                        taskIndex),
+                QUICK_SWITCH_FROM_HOME_FAILED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Quick switch from home failed: TaskViews at indices %d and 0 are missing.",
+                taskIndex);
+    }
+
+    public static void logFinishRecentsAnimation(boolean toRecents) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "finishRecentsAnimation: %b", toRecents),
+                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation: %b", toRecents);
+    }
+
+    public static void logSetEndTarget(@NonNull String target) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "setEndTarget %s", target), /* gestureEvent= */ SET_END_TARGET);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "setEndTarget %s", target);
+    }
+
+    public static void logStartHomeIntent(@NonNull String reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "OverviewComponentObserver.startHomeIntent: %s", reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewComponentObserver.startHomeIntent: %s", reason);
+    }
+
+    public static void logRunningTaskPackage(@NonNull String packageName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Current running task package name=%s", packageName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Current running task package name=%s", packageName);
+    }
+
+    public static void logSysuiStateFlags(@NonNull String stateFlags) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Current SystemUi state flags=%s", stateFlags));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Current SystemUi state flags=%s", stateFlags);
+    }
+
+    public static void logSetInputConsumer(@NonNull String consumerName, @NonNull String reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "setInputConsumer: %s. reason(s):%s", consumerName, reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "setInputConsumer: %s. reason(s):%s", consumerName, reason);
+    }
+
+    public static void logUpdateGestureStateRunningTask(
+            @NonNull String otherTaskPackage, @NonNull String runningTaskPackage) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Changing active task to %s because the previous task running on top of this "
+                        + "one (%s) was excluded from recents",
+                otherTaskPackage,
+                runningTaskPackage));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Changing active task to %s because the previous task running on top of this "
+                        + "one (%s) was excluded from recents",
+                otherTaskPackage,
+                runningTaskPackage);
+    }
+
+    public static void logOnInputEventActionUp(
+            int x, int y, int action, @NonNull String classification) {
+        String actionString = MotionEvent.actionToString(action);
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onMotionEvent(%d, %d): %s, %s", x, y, actionString, classification),
+                /* gestureEvent= */ action == ACTION_DOWN
+                        ? MOTION_DOWN
+                        : MOTION_UP);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onMotionEvent(%d, %d): %s, %s", x, y, actionString, classification);
+    }
+
+    public static void logOnInputEventActionMove(
+            @NonNull String action, @NonNull String classification, int pointerCount) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                        "onMotionEvent: %s, %s, pointerCount: %d",
+                        action,
+                        classification,
+                        pointerCount),
+                MOTION_MOVE);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onMotionEvent: %s, %s, pointerCount: %d", action, classification, pointerCount);
+    }
+
+    public static void logOnInputEventGenericAction(
+            @NonNull String action, @NonNull String classification) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onMotionEvent: %s, %s", action, classification));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "onMotionEvent: %s, %s", action, classification);
+    }
+
+    public static void logOnInputEventNavModeSwitched(
+            @NonNull String startNavMode, @NonNull String currentNavMode) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onInputEvent: Navigation mode switched mid-gesture (%s -> %s); "
+                        + "cancelling gesture.",
+                        startNavMode,
+                        currentNavMode),
+                NAVIGATION_MODE_SWITCHED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Navigation mode switched mid-gesture (%s -> %s); "
+                        + "cancelling gesture.",
+                startNavMode,
+                currentNavMode);
+    }
+
+    public static void logUnknownInputEvent(@NonNull String event) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onInputEvent: Cannot process input event: received unknown event %s", event));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Cannot process input event: received unknown event %s", event);
+    }
+
+    public static void logFinishRunningRecentsAnimation(boolean toHome) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "finishRunningRecentsAnimation: %b", toHome));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRunningRecentsAnimation: %b", toHome);
+    }
+
+    public static void logOnRecentsAnimationStartCancelled() {
+        ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onAnimationStart (canceled): 0",
+                /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationStart (canceled): 0");
+    }
+
+    public static void logOnRecentsAnimationStart(int appCount) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "RecentsAnimationCallbacks.onAnimationStart (canceled): %d", appCount),
+                /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "RecentsAnimationCallbacks.onAnimationStart (canceled): %d", appCount);
+    }
+
+    public static void logStartRecentsAnimationCallback(@NonNull String callback) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TaskAnimationManager.startRecentsAnimation(%s): "
+                        + "Setting mRecentsAnimationStartPending = false",
+                callback));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TaskAnimationManager.startRecentsAnimation(%s): "
+                        + "Setting mRecentsAnimationStartPending = false",
+                callback);
+    }
+
+    public static void logSettingRecentsAnimationStartPending(boolean value) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TaskAnimationManager.startRecentsAnimation: "
+                        + "Setting mRecentsAnimationStartPending = %b",
+                value));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TaskAnimationManager.startRecentsAnimation: "
+                        + "Setting mRecentsAnimationStartPending = %b",
+                value);
+    }
+
+    public static void logLaunchingSideTask(int taskId) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launching side task id=%d", taskId));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Launching side task id=%d", taskId);
+    }
+
+    public static void logOnInputEventActionDown(@NonNull ActiveGestureLog.CompoundString reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onMotionEvent: ").append(reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", reason.toString());
+    }
+
+    public static void logStartNewTask(@NonNull ActiveGestureLog.CompoundString tasks) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launching task: ").append(tasks));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", tasks.toString());
+    }
+
+    public static void logMotionPauseDetectorEvent(@NonNull ActiveGestureLog.CompoundString event) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "MotionPauseDetector: ").append(event));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "MotionPauseDetector: %s", event.toString());
+    }
+
+    public static void logHandleTaskAppearedFailed(
+            @NonNull ActiveGestureLog.CompoundString reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "handleTaskAppeared check failed: ").append(reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "handleTaskAppeared check failed: %s", reason.toString());
+    }
+
+    /**
+     * This is for special cases where the string is purely dynamic and therefore has no format that
+     * can be extracted. Do not use in any other case.
+     */
+    public static void logDynamicString(
+            @NonNull String string,
+            @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
+        ActiveGestureLog.INSTANCE.addLog(string, gestureEvent);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "%s", string);
+    }
+
+    public static void logOnSettledOnEndTarget(@NonNull String endTarget) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onSettledOnEndTarget %s", endTarget),
+                /* gestureEvent= */ ON_SETTLED_ON_END_TARGET);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "onSettledOnEndTarget %s", endTarget);
+    }
+
+    public static void logOnCalculateEndTarget(float velocityX, float velocityY, double angle) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f",
+                        velocityX,
+                        velocityY,
+                        angle),
+                velocityX == 0 && velocityY == 0 ? INVALID_VELOCITY_ON_SWIPE_UP : null);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f",
+                velocityX,
+                velocityY,
+                angle);
+    }
+
+    public static void logUnexpectedTaskAppeared(int taskId, @NonNull String packageName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s",
+                taskId,
+                packageName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s",
+                taskId,
+                packageName);
+    }
+}
diff --git a/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java
new file mode 100644
index 0000000..d0863f8
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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");
+
+    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 int LOG_START_ID =
+                (int) (UUID.nameUUIDFromBytes(QuickstepProtoLogGroup.class.getName().getBytes())
+                        .getMostSignificantBits() % Integer.MAX_VALUE);
+    }
+}
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..37a07c3
--- /dev/null
+++ b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.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.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)
+        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")
+        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
index eb459ff..47f393f 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
@@ -15,17 +15,10 @@
  */
 package com.android.launcher3.taskbar.bubbles
 
-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 androidx.test.core.app.ApplicationProvider
-import com.android.launcher3.R
-import com.android.wm.shell.shared.bubbles.BubbleInfo
+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
@@ -50,7 +43,7 @@
             DeviceEmulationSpec.forDisplays(
                 Displays.Phone,
                 isDarkTheme = false,
-                isLandscape = false
+                isLandscape = false,
             )
     }
 
@@ -58,7 +51,7 @@
     val screenshotRule =
         ViewScreenshotTestRule(
             emulationSpec,
-            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec))
+            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
         )
 
     @Test
@@ -86,39 +79,16 @@
     }
 
     private fun setupBubbleView(suppressNotification: Boolean = false): BubbleView {
-        val inflater = LayoutInflater.from(context)
-
-        val iconSize = 100
-        // 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 = Color.LTGRAY)
-        val badge = createCircleBitmap(radius = badgeRadius.toInt(), color = Color.RED)
-
-        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)
-        val bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null) as BubbleView
-        val dotPath =
-            PathParser.createPathFromPathData(
-                context.resources.getString(com.android.internal.R.string.config_icon_mask)
+        val bubbleView =
+            FakeBubbleViewFactory.createBubble(
+                context,
+                key = "key",
+                parent = null,
+                iconSize = 100,
+                iconColor = Color.LTGRAY,
+                suppressNotification = suppressNotification,
             )
-        val bubble =
-            BubbleBarBubble(bubbleInfo, bubbleView, badge, icon, Color.BLUE, dotPath, "test app")
-        bubbleView.setBubble(bubble)
         bubbleView.showDotIfNeeded(1f)
         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/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..50e2dcf
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.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.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(
+                    senderAvatar = null,
+                    senderName = "sender",
+                    message = "message",
+                    isGroupChat = false,
+                )
+            ) {}
+            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(
+                    senderAvatar = null,
+                    senderName = "sender",
+                    message = "message",
+                    isGroupChat = false,
+                )
+            ) {}
+            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(
+                    senderAvatar = null,
+                    senderName = "sender",
+                    message = "really, really, really, really, really long message. like really.",
+                    isGroupChat = false,
+                )
+            ) {}
+            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(
+                    senderAvatar = ColorDrawable(Color.RED),
+                    senderName = "sender",
+                    message = "message",
+                    isGroupChat = true,
+                )
+            ) {}
+            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(
+                    senderAvatar = ColorDrawable(Color.RED),
+                    senderName = "sender",
+                    message = "message",
+                    isGroupChat = true,
+                )
+            ) {}
+            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(
+                    senderAvatar = ColorDrawable(Color.RED),
+                    senderName = "sender",
+                    message = "really, really, really, really, really long message. like really.",
+                    isGroupChat = true,
+                )
+            ) {}
+            flyout.updateExpansionProgress(1f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_collapsed_onLeft() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onLeft") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    senderAvatar = ColorDrawable(Color.RED),
+                    senderName = "sender",
+                    message = "collapsed on left",
+                    isGroupChat = true,
+                )
+            ) {}
+            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(
+                    senderAvatar = ColorDrawable(Color.RED),
+                    senderName = "sender",
+                    message = "collapsed on right",
+                    isGroupChat = true,
+                )
+            ) {}
+            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(
+                    senderAvatar = ColorDrawable(Color.RED),
+                    senderName = "sender",
+                    message = "collapsed 90% on left",
+                    isGroupChat = true,
+                )
+            ) {}
+            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(
+                    senderAvatar = ColorDrawable(Color.RED),
+                    senderName = "sender",
+                    message = "collapsed 80% on right",
+                    isGroupChat = true,
+                )
+            ) {}
+            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
+    }
+}
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..f3fff9f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.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.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.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelTablet2023"])
+class TaskbarAutohideSuspendControllerTest {
+
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+    @get:Rule(order = 0) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 1)
+    val systemUiProxyRule = TestRule { base, _ ->
+        object : Statement() {
+            override fun evaluate() {
+                getInstrumentation().runOnMainSync {
+                    context.applicationContext.putObject(
+                        SystemUiProxy.INSTANCE,
+                        object : SystemUiProxy(context) {
+                            override fun notifyTaskbarAutohideSuspend(suspend: Boolean) {
+                                latestSuspendNotification = suspend
+                            }
+                        },
+                    )
+                }
+                base.evaluate()
+            }
+        }
+    }
+    @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/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 399aea6..02d6218 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.systemui.contextualeducation.GestureType;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,6 +55,10 @@
 
     @Mock
     SystemUiProxy mockSystemUiProxy;
+
+    @Mock
+    ContextualEduStatsManager mockContextualEduStatsManager;
+
     @Mock
     TouchInteractionService mockService;
     @Mock
@@ -100,6 +107,7 @@
                 mockService,
                 mCallbacks,
                 mockSystemUiProxy,
+                mockContextualEduStatsManager,
                 mockHandler,
                 mockAssistUtils);
     }
@@ -111,6 +119,13 @@
     }
 
     @Test
+    public void testPressBack_updateContextualEduData() {
+        mNavButtonController.onButtonClick(BUTTON_BACK, mockView);
+        verify(mockContextualEduStatsManager, times(1))
+                .updateEduStats(/* isTrackpad= */ eq(false), eq(GestureType.BACK));
+    }
+
+    @Test
     public void testPressImeSwitcher() {
         mNavButtonController.init(mockTaskbarControllers);
         mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView);
@@ -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..3524961
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+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.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 {
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+    @get:Rule(order = 0) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 2) 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()
+
+    @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
+    @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
+    @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() {
+        var backPressed = false
+        context.applicationContext.putObject(
+            SystemUiProxy.INSTANCE,
+            object : SystemUiProxy(context) {
+                override fun onBackPressed() {
+                    backPressed = true
+                }
+            },
+        )
+
+        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..e736446
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
@@ -0,0 +1,681 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.animation.AnimatorTestRule
+import android.platform.test.annotations.EnableFlags
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+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.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.BubbleControllers
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarPinningPreferenceRule
+import com.android.launcher3.taskbar.rules.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 java.util.Optional
+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(["pixelTablet2023"])
+class TaskbarStashControllerTest {
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+    @get:Rule(order = 0) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 1) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context)
+    @get:Rule(order = 2) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 3) 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 bubbleControllers: Optional<BubbleControllers>
+
+    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() {
+        taskbarPinningPreferenceRule.isPinned = true
+        activityContext.controllers.sharedState?.taskbarWasPinned = true
+
+        taskbarPinningPreferenceRule.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
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithBubbles_bubbleBarUnstashes() {
+        getInstrumentation().runOnMainSync {
+            bubbleControllers.get().bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleControllers.get().bubbleStashController.stashBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(false, true)
+        }
+        assertThat(bubbleControllers.get().bubbleStashController.isStashed).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithoutBubbles_bubbleBarStashed() {
+        getInstrumentation().runOnMainSync {
+            bubbleControllers.get().bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleControllers.get().bubbleStashController.stashBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(false, false)
+        }
+        assertThat(bubbleControllers.get().bubbleStashController.isStashed).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithBubbles_bubbleBarStashes() {
+        getInstrumentation().runOnMainSync {
+            bubbleControllers.get().bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleControllers.get().bubbleStashController.showBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(true, true)
+        }
+        assertThat(bubbleControllers.get().bubbleStashController.isStashed).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithoutBubbles_bubbleBarUnstashed() {
+        getInstrumentation().runOnMainSync {
+            bubbleControllers.get().bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleControllers.get().bubbleStashController.showBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(true, false)
+        }
+        assertThat(bubbleControllers.get().bubbleStashController.isStashed).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_bubbleBarExpandedBeforeTimeout_expandedAfterwards() {
+        getInstrumentation().runOnMainSync {
+            bubbleControllers.get().bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleControllers.get().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(bubbleControllers.get().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/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
index 43d924a..f783e40 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
@@ -199,8 +199,8 @@
         assertThat(editText?.hasFocus()).isTrue()
     }
 
-    private companion object {
-        private val TEST_APPS =
+    companion object {
+        val TEST_APPS =
             Array(16) {
                 AppInfo(
                     ComponentName(
@@ -213,6 +213,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..04f02e9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.TaskbarPreferenceRule
+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 {
+
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+    @get:Rule(order = 0) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 1)
+    val allAppsVisitedPreferenceRule =
+        TaskbarPreferenceRule(context, ALL_APPS_VISITED_COUNT.prefItem)
+    @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var overlayController: TaskbarOverlayController
+    @InjectController lateinit var stashController: TaskbarStashController
+
+    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() {
+        allAppsVisitedPreferenceRule.value = 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() {
+        allAppsVisitedPreferenceRule.value = ALL_APPS_VISITED_COUNT.maxCount
+        val viewController = createViewController()
+        getInstrumentation().runOnMainSync { viewController.show(false) }
+
+        val appsView = overlayController.requestWindow().appsView
+        getInstrumentation().runOnMainSync {
+            appsView.floatingHeaderView
+                .findFixedRowByType(PredictionRowView::class.java)
+                .setPredictedApps(TEST_PREDICTED_APPS)
+        }
+
+        val dividerView =
+            appsView.floatingHeaderView.findFixedRowByType(AppsDividerView::class.java)
+        assertThat(dividerView.dividerType).isEqualTo(DividerType.LINE)
+    }
+
+    private fun createViewController(): TaskbarAllAppsViewController {
+        return TestUtil.getOnUiThread {
+            val overlayContext = overlayController.requestWindow()
+            TaskbarAllAppsViewController(
+                overlayContext,
+                overlayContext.layoutInflater.inflate(
+                    R.layout.taskbar_all_apps_sheet,
+                    overlayContext.dragLayer,
+                    false,
+                ) as TaskbarAllAppsSlideInView,
+                taskbarUnitTestRule.activityContext.controllers,
+                searchSessionController,
+                /* showKeyboard= */ false, // Covered in TaskbarAllAppsControllerTest.
+            )
+        }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
index 785ec66..c8f50f7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
@@ -49,6 +49,7 @@
     @Mock private lateinit var bubbleDismissController: BubbleDismissController
     @Mock private lateinit var bubbleBarPinController: BubbleBarPinController
     @Mock private lateinit var bubblePinController: BubblePinController
+    @Mock private lateinit var bubbleBarSwipeController: BubbleBarSwipeController
     @Mock private lateinit var bubbleCreator: BubbleCreator
 
     @Mock private lateinit var motionEvent: MotionEvent
@@ -67,7 +68,8 @@
                 bubbleDismissController,
                 bubbleBarPinController,
                 bubblePinController,
-                bubbleCreator
+                Optional.of(bubbleBarSwipeController),
+                bubbleCreator,
             )
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
new file mode 100644
index 0000000..3b6952d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class BubbleBarSwipeControllerTest {
+
+    companion object {
+        const val UNSTASH_THRESHOLD = 100
+        const val EXPAND_THRESHOLD = 200
+        const val MAX_OVERSCROLL = 300
+        const val STASH_THRESHOLD = 50
+
+        const val UP_BELOW_UNSTASH = -UNSTASH_THRESHOLD + 10f
+        const val UP_ABOVE_UNSTASH = -UNSTASH_THRESHOLD - 10f
+        const val UP_ABOVE_EXPAND = -EXPAND_THRESHOLD - 10f
+        const val DOWN_UNDER_STASH = STASH_THRESHOLD - 10f
+        const val DOWN_OVER_STASH = STASH_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 expandThreshold: Int
+                    get() = EXPAND_THRESHOLD
+
+                override val maxOverscroll: Int
+                    get() = MAX_OVERSCROLL
+
+                override val stashThreshold: Int
+                    get() = STASH_THRESHOLD
+            }
+        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_stashedBar_aboveExpandThreshold_viewsHaveDampedTranslation() {
+        setUpStashedBar()
+        testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_EXPAND)
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() {
+        setUpCollapsedBar()
+        testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH)
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_aboveExpandThreshold_viewsHaveDampedTranslation() {
+        setUpCollapsedBar()
+        testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_EXPAND)
+    }
+
+    @Test
+    fun swipeDown_collapsedBar_belowStashThreshold_viewsHaveDampedTranslation() {
+        setUpCollapsedBar()
+        testViewsHaveDampedTranslationOnSwipe(DOWN_UNDER_STASH)
+    }
+
+    @Test
+    fun swipeDown_collapsedBar_overStashThreshold_viewsHaveDampedTranslation() {
+        setUpCollapsedBar()
+        testViewsHaveDampedTranslationOnSwipe(DOWN_OVER_STASH)
+    }
+
+    // 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_stashedBar_aboveExpandThreshold_animateTranslationToZeroOnFinish() {
+        setUpStashedBar()
+        testViewsTranslationResetOnFinish(UP_ABOVE_EXPAND)
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() {
+        setUpCollapsedBar()
+        testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH)
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_aboveExpandThreshold_animateTranslationToZeroOnFinish() {
+        setUpCollapsedBar()
+        testViewsTranslationResetOnFinish(UP_ABOVE_EXPAND)
+    }
+
+    @Test
+    fun swipeDown_collapsedBar_aboveStashThreshold_animateTranslationToZeroOnFinish() {
+        setUpCollapsedBar()
+        testViewsTranslationResetOnFinish(DOWN_OVER_STASH)
+    }
+
+    // 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_aboveUnstashThreshold_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_unstashBubbleBarOnce() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_overExpandThreshold_doesNotExpandBeforeFinish() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
+        }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+        getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = true)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_overExpandThreshold_isSwipeGestureTrue() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
+        }
+        assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue()
+    }
+
+    @Test
+    fun swipeUp_stashedBar_overExpandThresholdAndBackDown_doesNotExpandAfterFinish() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+        getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+    }
+
+    @Test
+    fun swipeDown_stashedBar_swipeIgnored() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
+        }
+        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_EXPAND)
+            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
+            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 swipeDown_collapsedBar_underStashThreshold_doesNotHideBar() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(DOWN_UNDER_STASH)
+            bubbleBarSwipeController.finish()
+        }
+        verify(bubbleStashController, never()).stashBubbleBar()
+    }
+
+    @Test
+    fun swipeDown_collapsedBar_overStashThreshold_doesNotHideBarBeforeFinish() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
+        }
+        verify(bubbleStashController, never()).stashBubbleBar()
+        getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
+        verify(bubbleStashController).stashBubbleBar()
+    }
+
+    @Test
+    fun swipeDown_collapsedBar_underStashThreshold_isSwipeGestureFalse() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(DOWN_UNDER_STASH)
+        }
+        assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse()
+    }
+
+    @Test
+    fun swipeDown_collapsedBar_overStashThreshold_isSwipeGestureTrue() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
+        }
+        assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue()
+    }
+
+    // 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/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
new file mode 100644
index 0000000..0f02444
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.widget.FrameLayout
+import android.widget.TextView
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for [BubbleBarFlyoutController] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleBarFlyoutControllerTest {
+
+    private lateinit var flyoutController: BubbleBarFlyoutController
+    private lateinit var flyoutContainer: FrameLayout
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val flyoutMessage =
+        BubbleBarFlyoutMessage(senderAvatar = null, "sender name", "message", isGroupChat = false)
+    private var onLeft = true
+
+    @Before
+    fun setUp() {
+        flyoutContainer = FrameLayout(context)
+        val positioner =
+            object : BubbleBarFlyoutPositioner {
+                override val isOnLeft
+                    get() = onLeft
+
+                override val targetTy = 50f
+                override val distanceToCollapsedPosition = PointF(100f, 200f)
+                override val collapsedSize = 30f
+                override val collapsedColor = Color.BLUE
+            }
+        flyoutController = BubbleBarFlyoutController(flyoutContainer, positioner)
+    }
+
+    @Test
+    fun flyoutPosition_left() {
+        flyoutController.setUpFlyout(flyoutMessage)
+        assertThat(flyoutContainer.childCount).isEqualTo(1)
+        val flyout = flyoutContainer.getChildAt(0)
+        val lp = flyout.layoutParams as FrameLayout.LayoutParams
+        assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT)
+        assertThat(flyout.translationY).isEqualTo(50f)
+    }
+
+    @Test
+    fun flyoutPosition_right() {
+        onLeft = false
+        flyoutController.setUpFlyout(flyoutMessage)
+        assertThat(flyoutContainer.childCount).isEqualTo(1)
+        val flyout = flyoutContainer.getChildAt(0)
+        val lp = flyout.layoutParams as FrameLayout.LayoutParams
+        assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT)
+        assertThat(flyout.translationY).isEqualTo(50f)
+    }
+
+    @Test
+    fun flyoutMessage() {
+        flyoutController.setUpFlyout(flyoutMessage)
+        assertThat(flyoutContainer.childCount).isEqualTo(1)
+        val flyout = flyoutContainer.getChildAt(0)
+        val sender = flyout.findViewById<TextView>(R.id.bubble_flyout_name)
+        assertThat(sender.text).isEqualTo("sender name")
+        val message = flyout.findViewById<TextView>(R.id.bubble_flyout_text)
+        assertThat(message.text).isEqualTo("message")
+    }
+
+    @Test
+    fun hideFlyout_removedFromContainer() {
+        flyoutController.setUpFlyout(flyoutMessage)
+        assertThat(flyoutContainer.childCount).isEqualTo(1)
+        flyoutController.hideFlyout()
+        assertThat(flyoutContainer.childCount).isEqualTo(0)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index 1d13956..d4a3b3a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -32,6 +32,7 @@
 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.util.MultiValueAlpha
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
@@ -55,8 +56,8 @@
 
     companion object {
         const val TASKBAR_BOTTOM_SPACE = 5
-        const val BUBBLE_BAR_WIDTH = 200f
-        const val BUBBLE_BAR_HEIGHT = 100f
+        const val BUBBLE_BAR_WIDTH = 200
+        const val BUBBLE_BAR_HEIGHT = 100
         const val HOTSEAT_TRANSLATION_Y = -45f
         const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE
         const val HANDLE_VIEW_WIDTH = 150
@@ -77,10 +78,12 @@
     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
@@ -105,7 +108,7 @@
             taskbarInsetsController,
             bubbleBarViewController,
             bubbleStashedHandleViewController,
-            ImmediateAction()
+            ImmediateAction(),
         )
     }
 
@@ -161,11 +164,13 @@
         mTransientBubbleStashController.isStashed = false
         whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
 
+        val bubbleInitialTranslation = bubbleView.translationY
+
         // When stash
         getInstrumentation().runOnMainSync {
             mTransientBubbleStashController.updateStashedAndExpandedState(
                 stash = true,
-                expand = false
+                expand = false,
             )
         }
 
@@ -181,9 +186,13 @@
         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
@@ -274,7 +283,7 @@
         val height = mTransientBubbleStashController.getTouchableHeight()
 
         // Then bubble bar height is returned
-        assertThat(height).isEqualTo(BUBBLE_BAR_HEIGHT.toInt())
+        assertThat(height).isEqualTo(BUBBLE_BAR_HEIGHT)
     }
 
     private fun advanceTimeBy(advanceMs: Long) {
@@ -285,20 +294,26 @@
     private fun setUpBubbleBarView() {
         getInstrumentation().runOnMainSync {
             bubbleBarView = BubbleBarView(context)
-            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+            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(0, 0)
+            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 */)
@@ -307,13 +322,16 @@
 
         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)
-        whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT)
+        whenever(bubbleBarViewController.bubbleBarCollapsedWidth)
+            .thenReturn(BUBBLE_BAR_WIDTH.toFloat())
+        whenever(bubbleBarViewController.bubbleBarCollapsedHeight)
+            .thenReturn(BUBBLE_BAR_HEIGHT.toFloat())
         whenever(bubbleBarViewController.createRevealAnimatorForStashChange(any()))
             .thenReturn(AnimatorSet())
     }
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 bbcf566..cb5e464 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
@@ -26,6 +26,7 @@
 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.TaskbarManager
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
@@ -143,6 +144,7 @@
                                     PendingIntent(IIntentSender.Default())
                                 },
                                 object : TaskbarNavButtonCallbacks {},
+                                DesktopVisibilityController(context),
                             ) {
                             override fun recreateTaskbar() {
                                 super.recreateTaskbar()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index 2a0aa4c..87a7cda 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -41,11 +41,14 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.util.SystemUiController;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
@@ -62,16 +65,14 @@
 import java.util.HashMap;
 
 public abstract class AbsSwipeUpHandlerTestCase<
-        RECENTS_CONTAINER extends Context & RecentsViewContainer,
-        STATE extends BaseState<STATE>,
-        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>,
+        RECENTS_CONTAINER extends Context & RecentsViewContainer & StatefulContainer<STATE>,
+        STATE extends BaseState<STATE>, RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>,
         ACTIVITY_TYPE extends  StatefulActivity<STATE> & RecentsViewContainer,
         ACTIVITY_INTERFACE extends BaseActivityInterface<STATE, ACTIVITY_TYPE>,
         SWIPE_HANDLER extends AbsSwipeUpHandler<RECENTS_CONTAINER, RECENTS_VIEW, STATE>> {
 
     protected final Context mContext =
             InstrumentationRegistry.getInstrumentation().getTargetContext();
-    protected final TaskAnimationManager mTaskAnimationManager = new TaskAnimationManager(mContext);
     protected final RecentsAnimationDeviceState mRecentsAnimationDeviceState =
             new RecentsAnimationDeviceState(mContext, true);
     protected final InputConsumerController mInputConsumerController =
@@ -105,6 +106,9 @@
             /* minimizedHomeBounds= */ null,
             new Bundle());
 
+    protected RecentsWindowManager mRecentsWindowManager;
+    protected TaskAnimationManager mTaskAnimationManager;
+
     @Mock protected ACTIVITY_INTERFACE mActivityInterface;
     @Mock protected ActivityInitListener<?> mActivityInitListener;
     @Mock protected RecentsAnimationController mRecentsAnimationController;
@@ -119,6 +123,16 @@
     public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
     @Before
+    public void setUpTaskAnimationManager() {
+        runOnMainSync(() -> {
+            if(Flags.enableFallbackOverviewInWindow()){
+                mRecentsWindowManager = new RecentsWindowManager(mContext);
+            }
+            mTaskAnimationManager = new TaskAnimationManager(mContext, mRecentsWindowManager);
+        });
+    }
+
+    @Before
     public void setUpRunningTaskInfo() {
         mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
index dd0b4b3..8d6906f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
@@ -30,7 +30,7 @@
 public class FallbackSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase<
         RecentsActivity,
         RecentsState,
-        FallbackRecentsView,
+        FallbackRecentsView<RecentsActivity>,
         RecentsActivity,
         FallbackActivityInterface,
         FallbackSwipeHandler> {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
index 7c48ea4..0a60774 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -34,6 +34,7 @@
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED
 import com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY
+import com.android.launcher3.util.DaggerSingletonTracker
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -62,6 +63,7 @@
     @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger
 
     @Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
+    @Mock private lateinit var mTracker: DaggerSingletonTracker
 
     private var mDefaultThemedIcons = false
     private var mDefaultAllowRotation = false
@@ -79,7 +81,7 @@
         // To match the default value of ALLOW_ROTATION
         LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false)
 
-        mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+        mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager, mTracker)
     }
 
     @After
@@ -90,7 +92,7 @@
 
     @Test
     fun loggingPrefs_correctDefaultValue() {
-        val systemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+        val systemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager, mTracker)
 
         assertThat(systemUnderTest.loggingPrefs[ALLOW_ROTATION_PREFERENCE_KEY]!!.defaultValue)
             .isFalse()
@@ -117,7 +119,7 @@
         LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = true)
 
         // This a new object so the values of mLoggablePrefs will be different
-        SettingsChangeLogger(mContext, mStatsLogManager).logSnapshot(mInstanceId)
+        SettingsChangeLogger(mContext, mStatsLogManager, mTracker).logSnapshot(mInstanceId)
 
         verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
         val capturedEvents = mEventCaptor.allValues
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index f31467f..a87465f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -20,6 +20,7 @@
 import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.util.TestDispatcherProvider
 import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest
 import com.android.quickstep.util.DesktopTask
@@ -36,17 +37,19 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
 class TasksRepositoryTest {
     private val tasks = (0..5).map(::createTaskWithId)
     private val defaultTaskList =
         listOf(
             GroupTask(tasks[0]),
             GroupTask(tasks[1], tasks[2], null),
-            DesktopTask(tasks.subList(3, 6))
+            DesktopTask(tasks.subList(3, 6)),
         )
     private val recentsModel = FakeRecentTasksDataSource()
     private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
@@ -65,7 +68,7 @@
             taskIconDataSource,
             taskVisualsChangedDelegate,
             testScope.backgroundScope,
-            TestDispatcherProvider(dispatcher)
+            TestDispatcherProvider(dispatcher),
         )
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
index fe67313..33d96a8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
@@ -70,6 +70,55 @@
         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(listOf(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)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index 99d3121..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.shared.split.SplitScreenConstants.SNAP_TO_30_70
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33
 import java.util.function.Consumer
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -59,23 +59,23 @@
 
     private lateinit var appPairsController: AppPairsController
 
-    private val left30: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_30_70)
+    private val left33: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_33_66)
     }
     private val left50: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_50_50)
+        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_50_50)
     }
-    private val left70: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_70_30)
+    private val left66: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_66_33)
     }
-    private val right30: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_30_70)
+    private val right33: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_33_66)
     }
     private val right50: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_50_50)
+        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_50_50)
     }
-    private val right70: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_70_30)
+    private val right66: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_66_33)
     }
 
     @Mock lateinit var mockAppPairIcon: AppPairIcon
@@ -113,26 +113,26 @@
 
     @Test
     fun shouldEncodeRankCorrectly() {
-        assertEquals("left + 30-70 should encode as 0 (0b0)", 0, left30)
+        assertEquals("left + 33-66 should encode as 0 (0b0)", 0, left33)
         assertEquals("left + 50-50 should encode as 1 (0b1)", 1, left50)
-        assertEquals("left + 70-30 should encode as 2 (0b10)", 2, left70)
+        assertEquals("left + 66-33 should encode as 2 (0b10)", 2, left66)
         // See AppPairsController#BITMASK_SIZE and BITMASK_FOR_SNAP_POSITION for context
-        assertEquals("right + 30-70 should encode as 1 followed by 16 0s", 1 shl 16, right30)
+        assertEquals("right + 33-66 should encode as 1 followed by 16 0s", 1 shl 16, right33)
         assertEquals("right + 50-50 should encode as the above value + 1", (1 shl 16) + 1, right50)
-        assertEquals("right + 70-30 should encode as the above value + 2", (1 shl 16) + 2, right70)
+        assertEquals("right + 66-33 should encode as the above value + 2", (1 shl 16) + 2, right66)
     }
 
     @Test
     fun shouldDecodeRankCorrectly() {
         assertEquals(
-            "left + 30-70 should decode to left",
+            "left + 33-66 should decode to left",
             STAGE_POSITION_TOP_OR_LEFT,
-            AppPairsController.convertRankToStagePosition(left30),
+            AppPairsController.convertRankToStagePosition(left33),
         )
         assertEquals(
-            "left + 30-70 should decode to 30-70",
-            SNAP_TO_30_70,
-            AppPairsController.convertRankToSnapPosition(left30),
+            "left + 33-66 should decode to 33-66",
+            SNAP_TO_2_33_66,
+            AppPairsController.convertRankToSnapPosition(left33),
         )
 
         assertEquals(
@@ -142,30 +142,30 @@
         )
         assertEquals(
             "left + 50-50 should decode to 50-50",
-            SNAP_TO_50_50,
+            SNAP_TO_2_50_50,
             AppPairsController.convertRankToSnapPosition(left50),
         )
 
         assertEquals(
-            "left + 70-30 should decode to left",
+            "left + 66-33 should decode to left",
             STAGE_POSITION_TOP_OR_LEFT,
-            AppPairsController.convertRankToStagePosition(left70),
+            AppPairsController.convertRankToStagePosition(left66),
         )
         assertEquals(
-            "left + 70-30 should decode to 70-30",
-            SNAP_TO_70_30,
-            AppPairsController.convertRankToSnapPosition(left70),
+            "left + 66-33 should decode to 66-33",
+            SNAP_TO_2_66_33,
+            AppPairsController.convertRankToSnapPosition(left66),
         )
 
         assertEquals(
-            "right + 30-70 should decode to right",
+            "right + 33-66 should decode to right",
             STAGE_POSITION_BOTTOM_OR_RIGHT,
-            AppPairsController.convertRankToStagePosition(right30),
+            AppPairsController.convertRankToStagePosition(right33),
         )
         assertEquals(
-            "right + 30-70 should decode to 30-70",
-            SNAP_TO_30_70,
-            AppPairsController.convertRankToSnapPosition(right30),
+            "right + 33-66 should decode to 33-66",
+            SNAP_TO_2_33_66,
+            AppPairsController.convertRankToSnapPosition(right33),
         )
 
         assertEquals(
@@ -175,19 +175,19 @@
         )
         assertEquals(
             "right + 50-50 should decode to 50-50",
-            SNAP_TO_50_50,
+            SNAP_TO_2_50_50,
             AppPairsController.convertRankToSnapPosition(right50),
         )
 
         assertEquals(
-            "right + 70-30 should decode to right",
+            "right + 66-33 should decode to right",
             STAGE_POSITION_BOTTOM_OR_RIGHT,
-            AppPairsController.convertRankToStagePosition(right70),
+            AppPairsController.convertRankToStagePosition(right66),
         )
         assertEquals(
-            "right + 70-30 should decode to 70-30",
-            SNAP_TO_70_30,
-            AppPairsController.convertRankToSnapPosition(right70),
+            "right + 66-33 should decode to 66-33",
+            SNAP_TO_2_66_33,
+            AppPairsController.convertRankToSnapPosition(right66),
         )
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
index 7b1c066..108cfb5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -66,7 +66,7 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_50_50
+                SplitScreenConstants.SNAP_TO_2_50_50
             )
         val task1 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
         val task2 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
@@ -81,7 +81,7 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_50_50
+                SplitScreenConstants.SNAP_TO_2_50_50
             )
         val splitBounds2 =
             SplitConfigurationOptions.SplitBounds(
@@ -89,7 +89,7 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_30_70
+                SplitScreenConstants.SNAP_TO_2_33_66
             )
         val task1 = GroupTask(createTask(1), createTask(2), splitBounds1, TaskViewType.GROUPED)
         val task2 = GroupTask(createTask(1), createTask(2), splitBounds2, TaskViewType.GROUPED)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index fc4c4f6..9d0b2ab 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
@@ -39,8 +38,7 @@
 import com.android.quickstep.util.SplitSelectStateController.SplitFromDesktopController
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.systemui.shared.recents.model.Task
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
-import java.util.function.Consumer
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
@@ -54,6 +52,7 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
+import java.util.function.Consumer
 
 @RunWith(AndroidJUnit4::class)
 class SplitSelectStateControllerTest {
@@ -63,7 +62,6 @@
     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()
@@ -87,7 +85,6 @@
         splitSelectStateController =
             SplitSelectStateController(
                 context,
-                handler,
                 stateManager,
                 depthController,
                 statsLogManager,
@@ -645,7 +642,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)
         )
     }
 
@@ -677,7 +674,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/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 88ffeea..b67bc5a 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -31,7 +31,6 @@
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.TaskItemInfo
-import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RecentsModel.RecentTasksChangedListener
 import com.android.quickstep.TaskIconCache
@@ -66,9 +65,7 @@
                 // Update canShowRunningAndRecentAppsAtInit before setUp() is called for each test.
                 canShowRunningAndRecentAppsAtInit =
                     description.methodName !in
-                        listOf(
-                            "canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled",
-                        )
+                        listOf("canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled")
             }
         }
 
@@ -76,7 +73,6 @@
     @Mock private lateinit var mockRecentsModel: RecentsModel
     @Mock private lateinit var mockContext: Context
     @Mock private lateinit var mockResources: Resources
-    @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
 
     private var taskListChangeId: Int = 1
 
@@ -100,13 +96,11 @@
             recentTasksChangedListener = null
             it
         }
-        recentAppsController =
-            TaskbarRecentAppsController(mockContext, mockRecentsModel) {
-                mockDesktopVisibilityController
-            }
+        recentAppsController = TaskbarRecentAppsController(mockContext, mockRecentsModel)
         recentAppsController.canShowRunningApps = canShowRunningAndRecentAppsAtInit
         recentAppsController.canShowRecentApps = canShowRunningAndRecentAppsAtInit
         recentAppsController.init(taskbarControllers)
+        taskbarControllers.onPostInit()
 
         recentTasksChangedListener =
             if (canShowRunningAndRecentAppsAtInit) {
@@ -133,7 +127,7 @@
         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)
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
         )
         verify(mockRecentsModel, never()).getTasks(any<Consumer<List<GroupTask>>>())
     }
@@ -147,7 +141,7 @@
         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)
+            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>>>())
@@ -162,7 +156,7 @@
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
                 runningTasks = emptyList(),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
             .containsExactlyElementsIn(hotseatPackages)
@@ -177,7 +171,7 @@
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
                 runningTasks = emptyList(),
-                recentTaskPackages = emptyList()
+                recentTaskPackages = emptyList(),
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
             .containsExactlyElementsIn(hotseatPackages)
@@ -191,7 +185,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 })
@@ -206,7 +200,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)
@@ -226,9 +220,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
@@ -251,7 +245,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 })
@@ -267,9 +261,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()
     }
@@ -281,7 +275,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()
     }
@@ -294,9 +288,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()
@@ -308,7 +302,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()
     }
@@ -321,7 +315,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))
@@ -335,7 +329,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).isEmpty()
         assertThat(recentAppsController.minimizedTaskIds).isEmpty()
@@ -349,7 +343,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
         assertThat(recentAppsController.minimizedTaskIds).isEmpty()
@@ -362,12 +356,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()
@@ -383,7 +377,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = runningTasks,
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).containsExactly(1, 2, 3)
         assertThat(recentAppsController.minimizedTaskIds).containsExactly(3)
@@ -397,7 +391,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1, task2),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
     }
@@ -410,13 +404,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 }
@@ -431,13 +425,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 }
@@ -452,17 +446,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
@@ -479,12 +473,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).
@@ -500,12 +494,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 =
@@ -519,12 +513,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).
@@ -540,12 +534,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))
@@ -557,12 +551,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).
@@ -579,7 +573,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(runningTask1, runningTask2),
-            recentTaskPackages = recentTaskPackages
+            recentTaskPackages = recentTaskPackages,
         )
 
         setInDesktopMode(true)
@@ -597,7 +591,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(runningTask1, runningTask2),
-            recentTaskPackages = recentTaskPackages
+            recentTaskPackages = recentTaskPackages,
         )
         setInDesktopMode(false)
         recentTasksChangedListener!!.onRecentTasksChanged()
@@ -613,7 +607,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.
@@ -629,7 +623,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
@@ -645,7 +639,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
@@ -661,14 +655,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()
     }
@@ -681,14 +675,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()
     }
@@ -702,7 +696,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1Minimized, task2Visible),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
 
@@ -710,7 +704,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
             runningTasks = listOf(task1Minimized, task2Minimized),
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         verify(taskbarViewController, times(2)).commitRunningAppsToUI()
@@ -726,7 +720,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = hotseatPackages,
             runningTasks = originalTasks,
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
 
@@ -734,7 +728,7 @@
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = hotseatPackages,
             runningTasks = newTasks,
-            recentTaskPackages = emptyList()
+            recentTaskPackages = emptyList(),
         )
 
         verify(taskbarViewController, times(2)).commitRunningAppsToUI()
@@ -751,10 +745,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 {
@@ -790,7 +781,7 @@
 
     private fun createTestAppInfo(
         packageName: String = "testPackageName",
-        className: String = "testClassName"
+        className: String = "testClassName",
     ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
 
     private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
@@ -800,7 +791,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.
@@ -818,14 +809,15 @@
                     Intent().apply { `package` = packageName },
                     ComponentName(packageName, "TestActivity"),
                     userHandle.identifier,
-                    0
+                    0,
                 )
             )
             .apply { this.isVisible = isVisible }
     }
 
     private fun setInDesktopMode(inDesktopMode: Boolean) {
-        whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
+        whenever(taskbarControllers.taskbarDesktopModeController.areDesktopTasksVisible)
+            .thenReturn(inDesktopMode)
     }
 
     private val GroupTask.packageNames: List<String>
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 885a7f6..231c113 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -24,6 +24,7 @@
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.AbstractFloatingViewHelper
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
 import com.android.launcher3.model.data.WorkspaceItemInfo
@@ -31,6 +32,7 @@
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.views.LauncherRecentsView
 import com.android.quickstep.views.TaskContainer
 import com.android.quickstep.views.TaskThumbnailViewDeprecated
@@ -67,7 +69,6 @@
     private val taskView: TaskView = mock()
     private val workspaceItemInfo: WorkspaceItemInfo = mock()
     private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock()
-    private val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = mock()
     private val iconView: TaskViewIcon = mock()
     private val transformingTouchDelegate: TransformingTouchDelegate = mock()
     private val factory: TaskShortcutFactory =
@@ -175,7 +176,7 @@
             .moveTaskToDesktop(
                 eq(taskContainer),
                 eq(DesktopModeTransitionSource.APP_FROM_OVERVIEW),
-                any()
+                any(),
             )
         verify(statsLogger).withItemInfo(workspaceItemInfo)
         verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP)
@@ -188,16 +189,19 @@
     }
 
     private fun createTaskContainer(task: Task): TaskContainer {
+        val snapshotView =
+            if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
+            else mock<TaskThumbnailViewDeprecated>()
         return TaskContainer(
             taskView,
             task,
-            thumbnailViewDeprecated,
+            snapshotView,
             iconView,
             transformingTouchDelegate,
             SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
             digitalWellBeingToast = null,
             showWindowsView = null,
-            overlayFactory
+            overlayFactory,
         )
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 2e456a7..2858929 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -248,7 +248,6 @@
     }
 
     @Test
-    @ScreenRecordRule.ScreenRecord // b/355042336
     public void testOverview() throws IOException {
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index 23a29f7..800fd4a 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;
 
@@ -117,7 +118,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 +142,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 +165,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/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 3e6436b..5ff2af7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -350,7 +350,6 @@
 
     @Test
     @TaskbarModeSwitch
-    @ScreenRecord // b/358607191
     public void testQuickSwitchToPreviousAppForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
         startTestActivity(2);
@@ -396,7 +395,6 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
     public void testQuickSwitchFromHome() throws Exception {
         startTestActivity(2);
         mLauncher.goHome().quickSwitchToPreviousApp();
@@ -578,7 +576,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")
diff --git a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
index 28c8a4a..ec07b93 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -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;
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/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/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_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/values-af/strings.xml b/res/values-af/strings.xml
index 490a7c2..4f62bda 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Stoor apppaar"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Hierdie apppaar word nie op hierdie toestel gesteun nie"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 7292eec..53dc4ba 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"የተከፈለ ማያ ገፅ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"የመተግበሪያ መረጃ ለ%1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"የ%1$s የአጠቃቀም ቅንብሮች"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"አዲስ መስኮት"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"የመተግበሪያ ጥምረትን ያስቀምጡ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ይህ የመተግበሪያ ጥምረት በዚህ መሣሪያ ላይ አይደገፍም"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 06fc0a8..04c2f2f 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"حفظ استخدام التطبيقين معًا"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | ‏<xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"لا يمكن استخدام هذين التطبيقَين في الوقت نفسه على هذا الجهاز"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index cd6e347..4d377c2 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"এপৰ পেয়াৰ ছেভ কৰক"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"এই ডিভাইচটোত এই এপ্‌ পেয়াৰ কৰাৰ সুবিধাটো সমৰ্থিত নহয়"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index b8d660f..4c2ba9b 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Tətbiq cütünü saxlayın"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Bu tətbiq cütü bu cihazda dəstəklənmir"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 4d4764e..002c800 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podeljeni ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji za: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Podešavanja potrošnje za %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Sačuvaj par aplikacija"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ovaj par aplikacija nije podržan na ovom uređaju"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 641509e..0984f32 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Падзелены экран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Інфармацыя пра праграму для: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s: налады выкарыстання"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Новае акно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Захаваць спалучэнне праграм"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Дадзенае спалучэнне праграм не падтрымліваецца на гэтай прыладзе"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 3ce3c5f..270374d 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Запазване на двойката приложения"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Тази двойка приложения не се поддържа на устройството"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 9b23590..5097784 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"অ্যাপ পেয়ার সেভ করুন"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"এই ডিভাইসে এই অ্যাপ পেয়ারটি কাজ করে না"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 4a34da7..2b168f6 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podijeljeni ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Postavke korištenja za: %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Sačuvaj par aplikacija"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Par aplikacija nije podržan na uređaju"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index c341ec7..77642f0 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Desa la parella d\'aplicacions"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Aquesta parella d\'aplicacions no s\'admet en aquest dispositiu"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index d3512c9..08747e0 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Uložit dvojici aplikací"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Tento pár aplikací není na tomto zařízení podporován"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 8aae860..0bf8514 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Gem appsammenknytning"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Denne appsammenknytning understøttes ikke på enheden"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 374f5a1..c3628ea 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"App-Paar speichern"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Dieses App-Paar wird auf diesem Gerät nicht unterstützt"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index cafe86e..85c6a31 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Αποθήκευση ζεύγους εφαρμογών"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Αυτό το ζεύγος εφαρμογών δεν υποστηρίζεται σε αυτή τη συσκευή"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 1b0722d..aee49be 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index de41d2c..8feccb0 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>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 1b0722d..aee49be 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 1b0722d..aee49be 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index a856340..769567c 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‎‏‏‎‎‏‎‏‎‎‏‎‏‏‎‎‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‏‎‏‎‎‏‏‏‏‏‏‎Split screen‎‏‎‎‏‎"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‎‏‏‏‎‏‏‏‏‏‏‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‏‎‏‎‎‎‎‏‎‎‎‏‎‏‎‎‎App info for %1$s‎‏‎‎‏‎"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‎‎‏‏‏‎‎‎‎‎‏‎‏‎‎‎‎‏‎‎‎‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‎‎‎‏‎‏‏‏‏‎‏‎Usage settings for %1$s‎‏‎‎‏‎"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‏‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‏‏‏‎‏‏‏‏‎‏‎‏‎‎‎‏‎‎‏‎‏‎‏‎‏‏‎New Window‎‏‎‎‏‎"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‎‎‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‏‎‎‏‏‎‎‎‎‏‏‏‎‎‎‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‎Save app pair‎‏‎‎‏‎"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‎‎‎‏‏‏‎‎‏‎‏‎‏‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‏‎‎‎‎‏‏‏‎‏‏‏‏‎‏‎‏‏‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎<xliff:g id="APP1">%1$s</xliff:g>‎‏‎‎‏‏‏‎ | ‎‏‎‎‏‏‎<xliff:g id="APP2">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‏‎‎‏‎‏‏‎‏‏‎‏‎‏‏‏‏‎‎‏‏‎‏‎‎‎‎‏‎‎‏‏‎‎‎‎‎‎‎This app pair isn\'t supported on this device‎‏‎‎‏‎"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index ba1b0af..e051843 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar vinculación"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"No se admite esta vinculación de apps en este dispositivo"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index ad12192..ddcee65 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Información de la aplicación %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Ajustes de uso para %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Ventana nueva"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar apps emparejadas"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"El dispositivo no admite esta aplicación emparejada"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 96d0b2c..844a0c7 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Salvesta rakendusepaar"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"See rakendusepaar ei ole selles seadmes toetatud"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index bc9b8c1..8c9375a 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Gorde aplikazio parea"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Aplikazio pare hori ez da onartzen gailu honetan"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index c167194..c0a6fe3 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ذخیره جفت برنامه"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"از این جفت برنامه در این دستگاه پشتیبانی نمی‌شود"</string>
@@ -130,7 +132,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>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 007d077..567a1ea 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Tallenna sovelluspari"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Sovellusparia ei tueta tällä laitteella"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index c443505..a886420 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Enr. paire d\'applis"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applis n\'est pas prise en charge sur cet appareil"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 4f5d111..d96bd98 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Enregistrer une paire d\'applis"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applications n\'est pas prise en charge sur cet appareil"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index ff7c029..c224a19 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Gardar parella de apps"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"O dispositivo non admite este emparellamento de aplicacións"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 872faef..732a8e4 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ઍપની જોડી સાચવો"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"આ ડિવાઇસ પર, આ ઍપની જોડીને સપોર્ટ આપવામાં આવતો નથી"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index a44b874..d4fb0ca 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ऐप पेयर सेव करें"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"साथ में इस्तेमाल किए जा सकने वाले ये ऐप्लिकेशन, इस डिवाइस पर काम नहीं कर सकते"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index d9f2072..a9fd14e 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podijeljeni zaslon"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Postavke upotrebe za %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Spremi par aplikacija"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Taj par aplikacija nije podržan na ovom uređaju"</string>
@@ -189,7 +190,7 @@
     <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 6bc8b70..1a38777 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Alkalmazáspár mentése"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ezt az alkalmazáspárt nem támogatja az eszköz"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 69b320d..eaaf435 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Պահել հավելվ. զույգը"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Հավելվածների զույգը չի աջակցվում այս սարքում"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 58a429f..62e9d9d 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Simpan pasangan aplikasi"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Pasangan aplikasi ini tidak didukung di perangkat ini"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 95bd21f..0014317 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Skipta skjá"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Upplýsingar um forrit fyrir %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Notkunarstillingar fyrir %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nýr gluggi"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Vista forritapar"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Þetta forritapar er ekki stutt í þessu tæki"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 3c01cd4..100da6c 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Salva coppia di app"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Questa coppia di app non è supportata su questo dispositivo"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index f198166..f85e571 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"שמירת צמד אפליקציות"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"צמד האפליקציות הזה לא נתמך במכשיר הזה"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index d2f9a97..fc89041 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割画面"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s のアプリ情報"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s の使用設定"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新しいウィンドウ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"アプリのペア設定を保存"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"このデバイスは、このアプリのペア設定に対応していません"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index e67cc41..f099bcd 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ეკრანის გაყოფა"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s-ის აპის ინფო"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"გამოყენების პარამეტრები %1$s-ისთვის"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ახალი ფანჯარა"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"აპთა წყვილის შენახვა"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ამ მოწყობილობაზე აღნიშნული აპთა წყვილი არ არის მხარდაჭერილი"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index d5ccae5..ebaacc9 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Экранды бөлу"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s қолданбасы туралы ақпарат"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s пайдалану параметрлері"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Жаңа терезе"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Қолданбаларды жұптау әрекетін сақтау"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Бұл құрылғы қолданбаларды жұптау функциясын қолдамайды."</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 3c9135b..b05eeb0 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"មុខងារ​បំបែកអេក្រង់"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"ព័ត៌មានកម្មវិធី​សម្រាប់ %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"ការកំណត់ការប្រើប្រាស់សម្រាប់ %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"វិនដូ​ថ្មី"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"រក្សាទុកគូកម្មវិធី"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"មិនអាចប្រើគូកម្មវិធីនេះនៅលើឧបករណ៍នេះបានទេ"</string>
@@ -117,7 +118,7 @@
     <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>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index ab84833..eda9fb8 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ಆ್ಯಪ್ ಪೇರ್ ಸೇವ್ ಮಾಡಿ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ಈ ಆ್ಯಪ್ ಜೋಡಿಯು ಈ ಸಾಧನದಲ್ಲಿ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 318cd00..94ebd15 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"앱 페어링 저장"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"이 앱 페어링은 이 기기에서 지원되지 않습니다"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 856e2b2..753d2dd 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Экранды бөлүү"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s колдонмосу жөнүндө маалымат"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s колдонмосун пайдалануу параметрлери"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Жаңы терезе"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Колдонмолорду сактап коюу"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Бул эки колдонмону бул түзмөктө бир маалда пайдаланууга болбойт"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 3d1a6c9..741556a 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ບັນທຶກຈັບຄູ່ແອັບ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ການຈັບຄູ່ແອັບນີ້ບໍ່ຮອງຮັບຢູ່ອຸປະກອນນີ້"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 4c9bd9b..6879862 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Išskaidyto ekrano režimas"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Programos „%1$s“ informacija"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"„%1$s“ naudojimo nustatymai"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Naujas langas"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Išsaugoti programų porą"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ši programų pora šiame įrenginyje nepalaikoma"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 0a82705..de00e4d 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Saglabāt lietotņu pāri"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Šis lietotņu pāris netiek atbalstīts šajā ierīcē"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 887ca82..91b30c9 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Зачувај го парот апликации"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Паров апликации не е поддржан на уредов"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index dda5679..b9e68e0 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"സ്‌ക്രീൻ വിഭജന മോഡ്"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s എന്നതിന്റെ ആപ്പ് വിവരങ്ങൾ"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s എന്നതിനുള്ള ഉപയോഗ ക്രമീകരണം"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"പുതിയ വിന്‍ഡോ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ആപ്പ് ജോടി സംരക്ഷിക്കുക"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ഈ ഉപകരണത്തിൽ ഈ ആപ്പ് ജോടിക്ക് പിന്തുണയില്ല"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 49d71c2..bba7e16 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Апп хослуулалтыг хадгалах"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Энэ апп хослуулалтыг уг төхөөрөмж дээр дэмждэггүй"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index fdf864a..548e764 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ॲपची जोडी सेव्ह करा"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"या ॲपची जोडीला या डिव्हाइसवर सपोर्ट नाही"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index b86f657..5ddf8a2 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Skrin pisah"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Maklumat apl untuk %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Tetapan penggunaan sebanyak %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Tetingkap Baharu"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Simpan gandingan apl"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Gandingan apl ini tidak disokong pada peranti ini"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 7e8fd14..e94dd8e 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"အက်ပ်တွဲချိတ်ခြင်း သိမ်းရန်"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ဤအက်ပ်တွဲချိတ်ခြင်းကို ဤစက်တွင် ပံ့ပိုးမထားပါ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 2a6611f..38003eb 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -31,7 +31,9 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
+    <string name="save_app_pair" msgid="5647523853662686243">"Lagre app-paret"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Denne apptilkoblingen støttes ikke på denne enheten"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Åpne enheten for å bruke denne apptilkoblingen"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index fa2e59b..4fae68b 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"स्प्लिट स्क्रिन"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s का हकमा एपसम्बन्धी जानकारी"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s को प्रयोगसम्बन्धी सेटिङ"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"नयाँ विन्डो"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"एपको पेयर सेभ गर्नुहोस्"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"यस डिभाइसमा यो एप पेयर प्रयोग गर्न मिल्दैन"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 9271b96..1c2bdc8 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"App-paar opslaan"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Dit app-paar wordt niet ondersteund op dit apparaat"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 98d52de..9a03926 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ଆପ ପେୟାର ସେଭ କରନ୍ତୁ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ଏହି ଆପ ପେୟାର ଏ ଡିଭାଇସରେ ସମର୍ଥିତ ନୁହେଁ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 782979e..461513c 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ਐਪ ਜੋੜਾਬੱਧ ਰੱਖਿਅਤ ਕਰੋ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ਇਸ ਐਪ ਜੋੜਾਬੱਧ ਦਾ ਇਸ ਡੀਵਾਈਸ \'ਤੇ ਸਮਰਥਨ ਨਹੀਂ ਕੀਤਾ ਜਾਂਦਾ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 71e569c..eeb9eb3 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Zapisz parę aplikacji"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ta para aplikacji nie jest obsługiwana na tym urządzeniu"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 1c44d9b..d2b214e 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ecrã dividido"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informações da app para %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Definições de utilização para %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nova janela"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar par de apps"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Este par de apps não é suportado neste dispositivo"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 3f44591..f864b3e 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Tela dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informações do app %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configurações de uso de %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nova janela"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salvar par de apps"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Este Par de apps não está disponível no dispositivo"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index b37d93b..fbab69c 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Salvează perechea de aplicații"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Perechea de aplicații nu este acceptată pe acest dispozitiv"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 321bf37..126e3ad 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Разделить экран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Сведения о приложении \"%1$s\""</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Настройки использования приложения \"%1$s\""</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Новое окно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Сохранить приложения"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Одновременно использовать эти два приложения на устройстве нельзя."</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 91c818a..328f2f5 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"බෙදුම් තිරය"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s සඳහා යෙදුම් තතු"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s සඳහා භාවිත සැකසීම්"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"නව කවුළුව"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"යෙදුම් යුගල සුරකින්න"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"මෙම යෙදුම් යුගලය මෙම උපාංගයෙහි සහාය නොදක්වයි"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index eaae7c0..c3b3422 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Uložiť pár aplikácií"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Tento pár aplikácií nie je v tomto zariadení podporovaný"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index dccf2f1..37a02e4 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Razdeljen zaslon"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Podatki o aplikaciji za: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nastavitve uporabe za »%1$s«"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novo okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Shrani par aplikacij"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ta par aplikacij ni podprt v tej napravi"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 84484e8..d3bfce6 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Ruaj çiftin e aplikacioneve"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ky çift aplikacionesh nuk mbështetet në këtë pajisje"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 64383f2..afaeb61 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Подељени екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Информације о апликацији за: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Подешавања потрошње за %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нови прозор"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Сачувај пар апликација"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Овај пар апликација није подржан на овом уређају"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 47aed86..547f60e 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Delad skärm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Appinformation för %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Användningsinställningar för %1$s"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nytt fönster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Spara app-par"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"De här apparna som ska användas tillsammans stöds inte på den här enheten"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 5eadd1d..7d2768b 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Hifadhi jozi ya programu"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Jozi hii ya programu haitumiki kwenye kifaa hiki"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index d84485a..2ae4c30 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ஆப்ஸ் ஜோடியைச் சேமி"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"இந்தச் சாதனத்தில் இந்த ஆப்ஸ் ஜோடி ஆதரிக்கப்படவில்லை"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 8487e96..75dc7f0 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"స్ప్లిట్ స్క్రీన్"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s కోసం యాప్ సమాచారం"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sకు సంబంధించిన వినియోగ సెట్టింగ్‌లు"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"కొత్త విండో"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"యాప్ పెయిర్‌ను సేవ్ చేయండి"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ఈ పరికరంలో ఈ యాప్ పెయిర్ సపోర్ట్ చేయదు"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 71f4d15..a6d3b7e 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"บันทึกคู่แอป"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ไม่รองรับคู่แอปนี้ในอุปกรณ์เครื่องนี้"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 7cf6a44..417962d 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"I-save ang app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Hindi sinusuportahan sa device na ito ang pares ng app na ito"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index d55181c..0caa1f9 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Uygulama çiftini kaydedin"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Bu uygulama çifti bu cihazda desteklenmiyor"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index b5c1c70..e980d22 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Зберегти пару додатків"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ці два додатки не можна одночасно використовувати на цьому пристрої"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 6fa76dd..4d59e17 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"ایپس کے جوڑے کو محفوظ کریں"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ایپس کا یہ جوڑا اس آلے پر تعاون یافتہ نہیں ہے"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 21a8145..d2225c7 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Ilova juftini saqlash"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Bu ilova jufti ushbu qurilmada ishlamaydi"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index b0bac73..4c14574 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Lưu cặp ứng dụng"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cặp ứng dụng này không hoạt động được trên thiết bị này"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 112b945..92591fb 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"保存应用组合"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"在该设备上无法使用此应用对"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index e63093e..05f3320 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"儲存應用程式配對"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"此裝置不支援此應用程式配對"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 25f9703..6ee3e99 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"儲存應用程式配對"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"這部裝置不支援這組應用程式配對"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index ec1f941..8d2e2f3 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -31,6 +31,8 @@
     <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>
+    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
+    <skip />
     <string name="save_app_pair" msgid="5647523853662686243">"Londoloza i-app ebhangqiwe"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Lokhu kubhanqwa kwe-app akusekelwa kule divayisi"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 507ce9a..701e64a 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>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index fd724a5..9d06021 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 -->
diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java
index fb8088c..e516ad0 100644
--- a/src/com/android/launcher3/Alarm.java
+++ b/src/com/android/launcher3/Alarm.java
@@ -20,6 +20,8 @@
 import android.os.Looper;
 import android.os.SystemClock;
 
+import androidx.annotation.VisibleForTesting;
+
 public class Alarm implements Runnable{
     // if we reach this time and the alarm hasn't been cancelled, call the listener
     private long mAlarmTriggerTime;
@@ -96,4 +98,13 @@
     public long getLastSetTimeout() {
         return mLastSetTimeout;
     }
+
+    /** Simulates the alarm firing for tests. */
+    @VisibleForTesting
+    public void finishAlarm() {
+        if (!mAlarmPending) return;
+        mAlarmPending = false;
+        mHandler.removeCallbacks(this);
+        mAlarmListener.onAlarm(this);
+    }
 }
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index fec94fe..2e75261 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -188,7 +188,7 @@
 
     public SystemUiController getSystemUiController() {
         if (mSystemUiController == null) {
-            mSystemUiController = new SystemUiController(getWindow());
+            mSystemUiController = new SystemUiController(getWindow().getDecorView());
         }
         return mSystemUiController;
     }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index cc5baea..4eca048 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -31,6 +31,8 @@
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp;
 import static com.android.launcher3.testing.shared.ResourceUtils.roundPxValueFromFloat;
+import static com.android.wm.shell.Flags.enableBubbleBar;
+import static com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar;
 import static com.android.wm.shell.Flags.enableTinyTaskbar;
 
 import android.annotation.SuppressLint;
@@ -64,8 +66,10 @@
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType;
 import com.android.launcher3.responsive.ResponsiveSpecsProvider;
 import com.android.launcher3.util.CellContentDimensions;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.IconSizeSteps;
+import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -219,6 +223,8 @@
     public int hotseatBarBottomSpacePx;
     public int hotseatBarEndOffset;
     public int hotseatQsbSpace;
+    public int inlineNavButtonsEndSpacingPx;
+    public int navButtonsLayoutWidthPx;
     public int springLoadedHotseatBarTopMarginPx;
     // These 2 values are only used for isVerticalBar
     // Padding between edge of screen and hotseat
@@ -233,7 +239,6 @@
     private final int mMinHotseatIconSpacePx;
     private final int mMinHotseatQsbWidthPx;
     private final int mMaxHotseatIconSpacePx;
-    public final int inlineNavButtonsEndSpacingPx;
     // Space required for the bubble bar between the hotseat and the edge of the screen. If there's
     // not enough space, the hotseat will adjust itself for the bubble bar.
     private final int mBubbleBarSpaceThresholdPx;
@@ -692,17 +697,12 @@
         if (areNavButtonsInline && !isPhone) {
             inlineNavButtonsEndSpacingPx =
                     res.getDimensionPixelSize(inv.inlineNavButtonsEndSpacing);
-            /*
-             * 3 nav buttons +
-             * Spacing between nav buttons +
-             * Space at the end for contextual buttons
-             */
-            hotseatBarEndOffset = 3 * res.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
-                    + 2 * res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween)
-                    + inlineNavButtonsEndSpacingPx;
-        } else {
-            inlineNavButtonsEndSpacingPx = 0;
-            hotseatBarEndOffset = 0;
+            /* 3 nav buttons + Spacing between nav buttons */
+            navButtonsLayoutWidthPx = 3 * res.getDimensionPixelSize(
+                    R.dimen.taskbar_nav_buttons_size)
+                    + 2 * res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween);
+            /* nav buttons layout width + Space at the end for contextual buttons */
+            hotseatBarEndOffset = navButtonsLayoutWidthPx + inlineNavButtonsEndSpacingPx;
         }
 
         mBubbleBarSpaceThresholdPx =
@@ -1056,6 +1056,7 @@
         return mHotseatColumnSpan;
     }
 
+    @VisibleForTesting
     public int getHotseatWidthPx() {
         return mHotseatWidthPx;
     }
@@ -2213,6 +2214,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));
@@ -2327,6 +2332,25 @@
     }
 
     /**
+     * Returns whether Taskbar and Hotseat should adjust horizontally on bubble bar location update.
+     */
+    public boolean shouldAdjustHotseatOnBubblesLocationUpdate(Context context) {
+        return enableBubbleBar()
+                && enableBubbleBarInPersistentTaskBar()
+                && DisplayController.getNavigationMode(context)
+                == NavigationMode.THREE_BUTTONS;
+    }
+
+    /** Returns hotseat translation X for the bubble bar position. */
+    public int getHotseatTranslationXForBubbleBar(boolean isNavbarOnRight) {
+        if (isNavbarOnRight) {
+            return 0;
+        } else {
+            return navButtonsLayoutWidthPx;
+        }
+    }
+
+    /**
      * Callback when a component changes the DeviceProfile associated with it, as a result of
      * configuration change
      */
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 6622e11..17084bb 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -60,6 +60,7 @@
         mScrollbar = scrollbar;
         mScrollbar.setRecyclerView(this);
         mScrollbar.setFastScrollerLocation(location);
+        scrollToTop();
         onUpdateScrollbar(0);
     }
 
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 0d4ebe0..ae4c122 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;
     }
 
     /**
@@ -270,21 +324,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 +354,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..5ea7bd9 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -354,7 +354,7 @@
      */
     @Deprecated
     public void reset(Context context) {
-        initGrid(context, getCurrentGridName(context));
+        initGrid(context, getDefaultGridName(context));
     }
 
     @VisibleForTesting
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 365e3d4..b0ec9b0 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -76,7 +76,6 @@
 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;
@@ -449,12 +448,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()
@@ -733,13 +730,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
@@ -1251,9 +1241,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
@@ -2582,10 +2570,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);
     }
 
     /**
@@ -2678,6 +2664,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();
diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java
index 8969b60..4c82e56 100644
--- a/src/com/android/launcher3/LauncherApplication.java
+++ b/src/com/android/launcher3/LauncherApplication.java
@@ -18,6 +18,7 @@
 import android.app.Application;
 
 import com.android.launcher3.dagger.DaggerLauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherBaseAppComponent;
 
 /**
@@ -30,10 +31,18 @@
     public void onCreate() {
         super.onCreate();
         MainProcessInitializer.initialize(this);
-        mAppComponent = DaggerLauncherAppComponent.builder().build();
+        initDagger();
     }
 
-    public LauncherBaseAppComponent getAppComponent() {
-        return mAppComponent;
+    public LauncherAppComponent getAppComponent() {
+        // 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;
+    }
+
+    protected void initDagger() {
+        mAppComponent = DaggerLauncherAppComponent.builder().appContext(this).build();
     }
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index ca1b2a9..7ad17d9 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -446,8 +447,8 @@
             IconCache iconCache = mApp.getIconCache();
             final IntSet removedIds = new IntSet();
             HashSet<WorkspaceItemInfo> archivedWorkspaceItemsToCacheRefresh = new HashSet<>();
-            boolean isAppArchived = PackageManagerHelper.INSTANCE.get(mApp.getContext())
-                    .isAppArchivedForUser(packageName, user);
+            boolean isAppArchived =
+                    new ApplicationInfoWrapper(mApp.getContext(), packageName, user).isArchived();
             synchronized (dataModel) {
                 if (isAppArchived) {
                     // Remove package icon cache entry for archived app in case of a session
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/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index d57f8a0..496d517 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()
@@ -312,7 +310,7 @@
         val info =
             PendingAddWidgetInfo(
                 widgetsListBaseEntry.mWidgets[0].widgetInfo,
-                LauncherSettings.Favorites.CONTAINER_DESKTOP
+                LauncherSettings.Favorites.CONTAINER_DESKTOP,
             )
         launcher.addPendingItem(
             info,
@@ -320,14 +318,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 (
@@ -354,7 +352,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/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/Utilities.java b/src/com/android/launcher3/Utilities.java
index fde7014..572274e 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;
@@ -122,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;
 
@@ -629,8 +626,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)
@@ -639,7 +635,7 @@
                 return null;
             } else {
                 ShortcutInfo si = siList.get(0);
-                mainIcon = ShortcutCachingLogic.getIcon(context, si,
+                mainIcon = CacheableShortcutInfo.getIcon(context, si,
                         appState.getInvariantDeviceProfile().fillResIconDpi);
                 // Only fetch badge if the icon is on workspace
                 if (info.id != ItemInfo.NO_ID && badge == null) {
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index cc4724c..1094768 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -22,8 +22,6 @@
 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;
@@ -679,18 +677,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) {
@@ -735,9 +728,7 @@
 
         removeCustomRules(rvContainer);
         removeCustomRules(getSearchRecyclerView());
-        if (!isSearchSupported()) {
-            layoutWithoutSearchContainer(rvContainer, showTabs);
-        } else if (isSearchBarFloating()) {
+        if (isSearchBarFloating()) {
             alignParentTop(rvContainer, showTabs);
             alignParentTop(getSearchRecyclerView(), /* tabs= */ false);
         } else {
@@ -768,9 +759,7 @@
         });
 
         removeCustomRules(mHeader);
-        if (!isSearchSupported()) {
-            layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */);
-        } else if (isSearchBarFloating()) {
+        if (isSearchBarFloating()) {
             alignParentTop(mHeader, false /* includeTabsMargin */);
         } else {
             layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */);
@@ -925,23 +914,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;
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 ae45a35..4e1e950 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -18,8 +18,6 @@
 import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT;
 import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
 
-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.logger.LauncherAtom.ContainerInfo;
 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN;
@@ -124,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;
         }
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index a4f130a..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);
         }
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 742648e..c6852e0 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -54,7 +54,6 @@
 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;
@@ -413,8 +412,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);
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index a2bd5dd..ac06ab4 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -34,7 +34,6 @@
 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;
@@ -220,15 +219,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,
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index ec45415..de3bb9e 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;
     }
 
@@ -142,13 +137,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 +145,6 @@
         mInput.reset();
         mInput.clearFocus();
         mQuery = null;
-        mInput.removeOnFocusChangeListener(this);
     }
 
     /**
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index aa8c5a5..8fe1b34 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -63,12 +63,6 @@
      * <p>
      */
     // 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 "
@@ -79,16 +73,7 @@
             "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_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 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");
@@ -104,10 +89,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,
@@ -154,32 +135,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.");
@@ -204,27 +164,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,
@@ -243,18 +183,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");
@@ -279,10 +208,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");
@@ -296,20 +221,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,
@@ -328,14 +248,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/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
index 3488c95..0a50e8b 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -16,6 +16,12 @@
 
 package com.android.launcher3.dagger;
 
+import android.content.Context;
+
+import com.android.launcher3.util.DaggerSingletonTracker;
+
+import dagger.BindsInstance;
+
 /**
  * Launcher base component for Dagger injection.
  *
@@ -25,8 +31,10 @@
  * See {@link LauncherAppComponent} for the one actually used by AOSP.
  */
 public interface LauncherBaseAppComponent {
+    DaggerSingletonTracker getDaggerSingletonTracker();
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
+        @BindsInstance Builder appContext(@ApplicationContext Context context);
         LauncherBaseAppComponent build();
     }
 }
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 85eb39b..a3cfe5c 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;
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 8b1f42b..a24f3ff 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -121,7 +121,7 @@
 
     @Override
     public void recreateControllers() {
-        mControllers = mActivity.createTouchControllers();
+        mControllers = mContainer.createTouchControllers();
     }
 
     public ViewGroupFocusHelper getFocusIndicatorHelper() {
@@ -134,15 +134,15 @@
     }
 
     private boolean isEventOverAccessibleDropTargetBar(MotionEvent ev) {
-        return isInAccessibleDrag() && isEventOverView(mActivity.getDropTargetBar(), ev);
+        return isInAccessibleDrag() && isEventOverView(mContainer.getDropTargetBar(), ev);
     }
 
     @Override
     public boolean onInterceptHoverEvent(MotionEvent ev) {
-        if (mActivity == null || mActivity.getWorkspace() == null) {
+        if (mContainer == null || mContainer.getWorkspace() == null) {
             return false;
         }
-        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (!(topView instanceof Folder)) {
             return false;
         } else {
@@ -197,7 +197,7 @@
 
 
     private boolean isInAccessibleDrag() {
-        return mActivity.getAccessibilityDelegate().isInAccessibleDrag();
+        return mContainer.getAccessibilityDelegate().isInAccessibleDrag();
     }
 
     @Override
@@ -210,12 +210,12 @@
 
     @Override
     public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
-        View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
+        View topView = AbstractFloatingView.getTopOpenViewWithType(mContainer,
                 AbstractFloatingView.TYPE_ACCESSIBLE);
         if (topView != null) {
             addAccessibleChildToList(topView, childrenForAccessibility);
             if (isInAccessibleDrag()) {
-                addAccessibleChildToList(mActivity.getDropTargetBar(), childrenForAccessibility);
+                addAccessibleChildToList(mContainer.getDropTargetBar(), childrenForAccessibility);
             }
         } else {
             super.addChildrenForAccessibility(childrenForAccessibility);
@@ -420,14 +420,14 @@
     public void onViewAdded(View child) {
         super.onViewAdded(child);
         updateChildIndices();
-        mActivity.onDragLayerHierarchyChanged();
+        mContainer.onDragLayerHierarchyChanged();
     }
 
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         updateChildIndices();
-        mActivity.onDragLayerHierarchyChanged();
+        mContainer.onDragLayerHierarchyChanged();
     }
 
     @Override
diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
index 0f3cad6..cc5e890 100644
--- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
+++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
@@ -69,7 +69,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;
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 374c07b..27ec838 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,19 +30,27 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.Message;
 import android.os.Messenger;
 import android.text.TextUtils;
-import android.util.ArrayMap;
 import android.util.Log;
-import android.util.Pair;
 
 import 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.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.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.ExecutionException;
 
 /**
  * Exposes various launcher grid options and allows the caller to change them.
@@ -86,11 +96,9 @@
     private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337;
     private static final int MESSAGE_ID_UPDATE_GRID = 7414;
 
-    /**
-     * 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() {
@@ -144,14 +152,20 @@
 
     @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: {
                 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;
                     }
@@ -160,15 +174,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:
@@ -176,6 +198,23 @@
         }
     }
 
+    /**
+     * 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",
@@ -191,16 +230,19 @@
     }
 
     private synchronized Bundle getPreview(Bundle request) {
-        PreviewLifecycleObserver observer = 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();
@@ -214,33 +256,21 @@
             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
@@ -260,7 +290,9 @@
                     }
                     break;
                 default:
-                    destroyObserver(this);
+                    // Unknown command, destroy lifecycle
+                    Log.d(TAG, "Unknown preview command: " + message.what + ", destroying preview");
+                    MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
                     break;
             }
 
@@ -269,16 +301,16 @@
 
         @Override
         public void binderDied() {
-            destroyObserver(this);
+            MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
         }
 
         /**
-         * Returns a key that should make the PreviewSurfaceRenderer unique and if two of them have
-         * the same key they will be treated as the same PreviewSurfaceRenderer. Primary this is
-         * used to prevent memory leaks by removing the old PreviewSurfaceRenderer.
+         * Two renderers are considered same if they have the same host token and display Id
          */
-        public Pair<IBinder, Integer> getIdentifier() {
-            return new Pair<>(renderer.getHostToken(), renderer.getDisplayId());
+        public boolean isSameRenderer(PreviewLifecycleObserver plo) {
+            return plo != null
+                    && plo.renderer.getHostToken().equals(renderer.getHostToken())
+                    && plo.renderer.getDisplayId() == renderer.getDisplayId();
         }
     }
 }
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 2408955..40c0cc6 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;
@@ -206,15 +206,12 @@
             mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel);
         }
 
-        if (Utilities.ATLEAST_S) {
-            WallpaperColors wallpaperColors = wallpaperColorsOverride != null
-                    ? wallpaperColorsOverride
-                    : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);
-            mWallpaperColorResources = wallpaperColors != null ? LocalColorExtractor.newInstance(
-                    context).generateColorsOverride(wallpaperColors) : null;
-        } else {
-            mWallpaperColorResources = null;
-        }
+        WallpaperColors wallpaperColors = wallpaperColorsOverride != null
+                ? wallpaperColorsOverride
+                : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);
+        mWallpaperColorResources = wallpaperColors != null
+                ? LocalColorExtractor.newInstance(context).generateColorsOverride(wallpaperColors)
+                : null;
         mAppWidgetHost = new LauncherPreviewAppWidgetHost(context);
     }
 
@@ -320,12 +317,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);
             }
         });
     }
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 56c4ca4..1b23d75 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -91,7 +91,7 @@
     private final int mDisplayId;
     private final Display mDisplay;
     private final WallpaperColors mWallpaperColors;
-    private final RunnableList mOnDestroyCallbacks = new RunnableList();
+    private final RunnableList mLifeCycleTracker;
 
     private final SurfaceControlViewHost mSurfaceControlViewHost;
 
@@ -100,8 +100,10 @@
     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) {
@@ -120,11 +122,13 @@
             throw new IllegalArgumentException("Display ID does not match any displays.");
         }
 
-        mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() ->
-                new SurfaceControlViewHost(mContext, context.getSystemService(DisplayManager.class)
-                        .getDisplay(DEFAULT_DISPLAY), mHostToken)
-        ).get(5, TimeUnit.SECONDS);
-        mOnDestroyCallbacks.add(mSurfaceControlViewHost::release);
+        mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() -> new MySurfaceControlViewHost(
+                mContext,
+                context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
+                mHostToken,
+                mLifeCycleTracker))
+                .get(5, TimeUnit.SECONDS);
+        mLifeCycleTracker.add(this::destroy);
     }
 
     public int getDisplayId() {
@@ -139,25 +143,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;
@@ -276,13 +273,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();
@@ -355,4 +350,24 @@
             mViewRoot.addView(view);
         }
     }
+
+    private static class MySurfaceControlViewHost extends SurfaceControlViewHost {
+
+        private final RunnableList mLifecycleTracker;
+
+        MySurfaceControlViewHost(Context context, Display display, IBinder hostToken,
+                RunnableList lifeCycleTracker) {
+            super(context, display, hostToken);
+            mLifecycleTracker = lifeCycleTracker;
+            mLifecycleTracker.add(this::release);
+        }
+
+        @Override
+        public void release() {
+            super.release();
+            // RunnableList ensures that the callback is only called once
+            MAIN_EXECUTOR.execute(mLifecycleTracker::executeAllAndDestroy);
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
index 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..c121340
--- /dev/null
+++ b/src/com/android/launcher3/icons/CacheableShortcutInfo.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.icons
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.LauncherActivityInfo
+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 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
+import kotlin.math.max
+
+/** 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 getDescription(info: CacheableShortcutInfo, fallback: CharSequence): CharSequence =
+        info.shortcutInfo.longLabel.let { if (TextUtils.isEmpty(it)) fallback else it!! }
+
+    override fun getLastUpdatedTime(info: CacheableShortcutInfo?, packageInfo: PackageInfo) =
+        info?.let { max(info.shortcutInfo.lastChangedTimestamp, packageInfo.lastUpdateTime) }
+            ?: packageInfo.lastUpdateTime
+
+    override fun addToMemCache() = false
+
+    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
+        }
+}
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..5396597 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -36,7 +36,6 @@
 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 +53,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.CachedObjectCachingLogic;
 import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
@@ -65,7 +65,6 @@
 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.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
@@ -97,12 +96,10 @@
 
     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 +109,13 @@
     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);
+        mComponentWithLabelCachingLogic = new CachedObjectCachingLogic(
+                context, false /* loadIcons */, false /* addToMemCache */);
+        mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.INSTANCE;
         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 -> { });
@@ -224,28 +220,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.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 +237,44 @@
         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.
      */
-    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.DEFAULT).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 +328,16 @@
         } else {
             Intent intent = info.getIntent();
             getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user),
-                    true, useLowResIcon, info.isArchived());
+                    true, useLowResIcon);
         }
     }
 
+    /**
+     * Loads and returns the icon for the provided object without adding it to memCache
+     */
     public synchronized String getTitleNoCache(ComponentWithLabel info) {
         CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info,
-                mComponentWithLabelCachingLogic, false /* usePackageIcon */,
-                true /* useLowResIcon */);
+                mComponentWithLabelCachingLogic, LookupFlag.USE_LOW_RES);
         return Utilities.trim(entry.title);
     }
 
@@ -351,40 +348,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, mLauncherActivityInfoCachingLogic, 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.
@@ -482,9 +454,8 @@
                                 /* user = */ sectionKey.first,
                                 () -> duplicateIconRequests.get(0).launcherActivityInfo,
                                 mLauncherActivityInfoCachingLogic,
-                                c,
-                                /* usePackageIcon= */ false,
-                                /* useLowResIcons = */ sectionKey.second);
+                                sectionKey.second ? LookupFlag.USE_LOW_RES : LookupFlag.DEFAULT,
+                                c);
 
                         for (IconRequestInfo<T> iconRequest : duplicateIconRequests) {
                             applyCacheEntry(entry, iconRequest.itemInfo);
@@ -600,28 +571,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,12 +602,6 @@
                 info.getAppLabel());
     }
 
-    @Override
-    @NonNull
-    protected String getIconSystemState(String packageName) {
-        return mIconProvider.getSystemStateForPackage(mSystemState, packageName);
-    }
-
     /**
      * Interface for receiving itemInfo with high-res icon.
      */
diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
deleted file mode 100644
index de2269c..0000000
--- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
+++ /dev/null
@@ -1,81 +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.Build;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.Flags;
-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)) {
-            IconOptions iconOptions = new IconOptions().setUser(object.getUser());
-            iconOptions.mIsArchived = Flags.useNewIconForArchivedApps()
-                && Build.VERSION.SDK_INT >= 35
-                && object.getActivityInfo().isArchived;
-            return li.createBadgedIconBitmap(
-                    LauncherAppState.getInstance(context)
-                        .getIconProvider()
-                        .getIcon(object, li.mFillResIconDpi),
-                    iconOptions
-            );
-        }
-    }
-}
diff --git a/src/com/android/launcher3/icons/Legacy.kt b/src/com/android/launcher3/icons/Legacy.kt
new file mode 100644
index 0000000..3bf3bb2
--- /dev/null
+++ b/src/com/android/launcher3/icons/Legacy.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.launcher3.icons
+
+import com.android.launcher3.icons.cache.CachedObject
+
+/**
+ * This files contains some definitions used during refactoring to avoid breaking changes.
+ *
+ * TODO(b/366237794) remove this file once refactoring is complete
+ */
+
+/** Temporary interface to allow easier refactoring */
+interface ComponentWithLabel : CachedObject<IconCache>
+
+/** Temporary interface to allow easier refactoring */
+interface ComponentWithLabelAndIcon : ComponentWithLabel
diff --git a/src/com/android/launcher3/icons/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/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/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index f54fc57..4c017e9 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
@@ -28,8 +29,6 @@
 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;
@@ -47,7 +46,6 @@
 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;
@@ -100,7 +98,7 @@
     @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;
@@ -121,13 +119,22 @@
             @NonNull DeviceGridState destDeviceState,
             @NonNull DatabaseHelper target,
             @NonNull SQLiteDatabase source) {
+
+        Log.i("b/360462379", "Going from " + srcDeviceState.getColumns() + "x"
+                + srcDeviceState.getRows());
+        Log.i("b/360462379", "Going to " + destDeviceState.getColumns() + "x"
+                + destDeviceState.getRows());
+
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
+            Log.i("b/360462379", "Does not need to migrate.");
             return true;
         }
 
-        if (Flags.enableGridMigrationFix()
+        if (LauncherPrefs.get(context).get(IS_FIRST_LOAD_AFTER_RESTORE)
+                && Flags.enableGridMigrationFix()
                 && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
                 && srcDeviceState.getRows() < destDeviceState.getRows()) {
+            Log.i("b/360462379", "Grid migration fix entry point.");
             // Only use this strategy when comparing the previous grid to the new grid and the
             // columns are the same and the destination has more rows
             copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
@@ -136,11 +143,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(),
@@ -340,23 +346,6 @@
                 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,
@@ -453,18 +442,15 @@
         private final SQLiteDatabase mDb;
         private final String mTableName;
         private final Context mContext;
-        private final Set<String> mValidPackages;
         private int mLastScreenId = -1;
 
         private final Map<Integer, ArrayList<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() {
@@ -496,7 +482,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: {
@@ -578,14 +563,12 @@
                         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());
 
                             LauncherAppWidgetProviderInfo pInfo = widgetManagerHelper
                                     .getLauncherAppWidgetInfo(entry.appWidgetId, cn);
@@ -648,7 +631,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<>());
@@ -665,27 +647,6 @@
         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> {
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 605accf..dff5463 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -23,6 +23,7 @@
 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 +70,13 @@
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderNameInfos;
 import com.android.launcher3.folder.FolderNameProvider;
+import com.android.launcher3.icons.CacheableShortcutCachingLogic;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
-import com.android.launcher3.icons.ComponentWithLabelAndIcon.ComponentWithIconCachingLogic;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.LauncherActivityCachingLogic;
-import com.android.launcher3.icons.ShortcutCachingLogic;
+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;
@@ -246,7 +248,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.
@@ -298,13 +300,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
@@ -322,8 +324,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();
@@ -360,7 +364,7 @@
             }
 
             updateHandler.updateIcons(allWidgetsList,
-                    new ComponentWithIconCachingLogic(mApp.getContext(), true),
+                    new CachedObjectCachingLogic(mApp.getContext()),
                     mApp.getModel()::onWidgetLabelsUpdated);
             logASplit("save widgets in icon cache");
 
@@ -397,7 +401,7 @@
     }
 
     protected void loadWorkspace(
-            List<ShortcutInfo> allDeepShortcuts,
+            List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
             LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger
@@ -423,7 +427,7 @@
     }
 
     private void loadWorkspaceImpl(
-            List<ShortcutInfo> allDeepShortcuts,
+            List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
             @Nullable LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger) {
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 2febb22..5464afe 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -119,7 +119,8 @@
         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: {
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/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..ac9f2d6 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -4,8 +4,6 @@
 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;
@@ -48,7 +46,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;
 
@@ -107,7 +105,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/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/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index f4dda55..361f09d 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -21,7 +21,6 @@
 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;
@@ -233,16 +232,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 &&
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index e66f496..124907f 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;
@@ -34,10 +33,10 @@
 import com.android.launcher3.SessionCommitReceiver;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.ItemInstallQueue;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 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;
@@ -171,8 +170,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 +243,8 @@
                 && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
                 && sessionInfo.getAppIcon() != null
                 && !TextUtils.isEmpty(sessionInfo.getAppLabel())
-                && !PackageManagerHelper.INSTANCE.get(mAppContext).isAppInstalled(
-                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+                && !new ApplicationInfoWrapper(mAppContext, sessionInfo.getAppPackageName(),
+                        getUserHandle(sessionInfo)).isInstalled();
     }
 
     public InstallSessionTracker registerInstallTracker(
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
index 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..3064abf 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -26,6 +26,7 @@
 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;
@@ -43,6 +44,7 @@
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
@@ -58,10 +60,20 @@
 
     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
@@ -89,6 +101,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());
@@ -120,7 +138,8 @@
         private final LauncherActivityInfo mInfo;
 
         public ShortcutConfigActivityInfoVO(LauncherActivityInfo info) {
-            super(info.getComponentName(), info.getUser());
+            super(info.getComponentName(), info.getUser(),
+                    new ApplicationInfoWrapper(info.getApplicationInfo()));
             mInfo = info;
         }
 
@@ -131,7 +150,7 @@
 
         @Override
         public Drawable getFullResIcon(IconCache cache) {
-            return cache.getFullResIcon(mInfo);
+            return cache.getFullResIcon(mInfo.getActivityInfo());
         }
 
         @Override
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index aa24f60..755c3eb 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -27,11 +27,13 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
@@ -113,6 +115,8 @@
         final ComponentName activity = originalInfo.getTargetComponent();
         final UserHandle user = originalInfo.user;
         return () -> {
+            ApplicationInfoWrapper infoWrapper =
+                    new ApplicationInfoWrapper(context, activity.getPackageName(), user);
             List<ShortcutInfo> shortcuts = new ShortcutRequest(context, user)
                     .withContainer(activity)
                     .query(ShortcutRequest.PUBLISHED);
@@ -121,7 +125,7 @@
             for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
                 final ShortcutInfo shortcut = shortcuts.get(i);
                 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, context);
-                cache.getShortcutIcon(si, shortcut);
+                cache.getShortcutIcon(si, new CacheableShortcutInfo(shortcut, infoWrapper));
                 si.rank = i;
                 si.container = CONTAINER_SHORTCUTS;
 
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 0c90eb9..63c9d94 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,5 +1,6 @@
 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;
@@ -7,7 +8,6 @@
 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;
@@ -35,12 +35,14 @@
 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;
 
@@ -184,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);
         }
@@ -334,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));
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 21897bf..775d248 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -75,7 +75,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;
@@ -127,12 +126,12 @@
 
         if (Flags.enableNarrowGridRestore()) {
             String oldPhoneFileName = idp.dbFile;
-            List<String> previousDbs = existingDbs();
+            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);
-            trySettingPreviousGidAsCurrent(context, idp, oldPhoneFileName, previousDbs);
+            trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
         } else {
             idp.reinitializeAfterRestore(context);
         }
@@ -143,7 +142,7 @@
      * 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);
@@ -166,17 +165,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 6ff51ca..82229f8 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -24,7 +24,6 @@
 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
@@ -78,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
@@ -91,7 +90,7 @@
                     context,
                     context.appsView.layoutInflater.cloneInContext(allAppsPreInflationContext),
                     null,
-                    null
+                    null,
                 ) {
                 override fun setAppsPerRow(appsPerRow: Int) = Unit
 
@@ -124,7 +123,7 @@
                     for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
                         putRecycledView(viewHolders[i])
                     }
-                }
+                },
             )
         mCancellableTask = task
         VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
@@ -144,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/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/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/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 28f2def..54b2eae 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -20,6 +20,7 @@
 
 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;
@@ -195,15 +196,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/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 6d9b891..3a93981 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: {
@@ -486,8 +479,9 @@
                 || LauncherAppState.getInstance(mContext).getModel().isModelLoaded();
     }
 
-    protected Activity getCurrentActivity() {
-        return Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+    protected WindowInsets getWindowInsets(){
+        return Launcher.ACTIVITY_TRACKER.getCreatedActivity().getWindow().getDecorView()
+                .getRootWindowInsets();
     }
 
     /**
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/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/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/DaggerSingletonObject.java b/src/com/android/launcher3/util/DaggerSingletonObject.java
new file mode 100644
index 0000000..b8cf2ae
--- /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 extends SafeCloseable> {
+    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..2946da1
--- /dev/null
+++ b/src/com/android/launcher3/util/DaggerSingletonTracker.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+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> mLauncherAppSingletons = new ArrayList<>();
+
+    @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) {
+        mLauncherAppSingletons.add(closeable);
+    }
+
+    @Override
+    public void close() {
+        // Destroy in reverse order
+        for (int i = mLauncherAppSingletons.size() - 1; i >= 0; i--) {
+            mLauncherAppSingletons.get(i).close();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index b1e82bb..c59cc81 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,7 +32,6 @@
 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;
@@ -42,7 +40,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.ArraySet;
 import android.util.Log;
@@ -132,21 +129,15 @@
         }
 
         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);
     }
 
@@ -161,7 +152,7 @@
                                     && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get(
                                     TASKBAR_PINNING_IN_DESKTOP_MODE);
                     if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) {
-                        handleInfoChange(mWindowContext.getDisplay());
+                        notifyConfigChange();
                     }
                 };
 
@@ -187,18 +178,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.
      */
@@ -222,6 +201,13 @@
         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;
@@ -257,36 +243,22 @@
         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) {
+            notifyConfigChange();
         }
     }
 
@@ -309,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
@@ -350,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) {
@@ -404,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<>());
@@ -465,6 +436,8 @@
             mIsTaskbarPinnedInDesktopMode = LauncherPrefs.get(displayInfoContext).get(
                     TASKBAR_PINNING_IN_DESKTOP_MODE);
             mIsInDesktopMode = wmProxy.isInDesktopMode();
+            mShowLockedTaskbarOnHome = wmProxy.showLockedTaskbarOnHome(displayInfoContext);
+            mIsHomeVisible = wmProxy.isHomeVisible(displayInfoContext);
         }
 
         /**
@@ -481,6 +454,10 @@
                 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;
                 }
@@ -496,10 +473,6 @@
             return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar();
         }
 
-        public boolean isInDesktopMode() {
-            return mIsInDesktopMode;
-        }
-
         /**
          * Returns {@code true} if the bounds represent a tablet.
          */
@@ -552,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/ExecutorUtil.java b/src/com/android/launcher3/util/ExecutorUtil.java
new file mode 100644
index 0000000..efc0eec
--- /dev/null
+++ b/src/com/android/launcher3/util/ExecutorUtil.java
@@ -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.launcher3.util;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.os.Looper;
+
+import java.util.concurrent.ExecutionException;
+
+public final class ExecutorUtil {
+
+    /**
+     * Executes runnable on {@link Looper#getMainLooper()}, otherwise fails with an exception.
+     */
+    public static void executeSyncOnMainOrFail(Runnable runnable) {
+        try {
+            MAIN_EXECUTOR.submit(runnable).get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index 1a0f9a0..a7d5c13 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> {
 
@@ -105,6 +108,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 context 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 +137,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 SandboxApplication {
 
         private static final String TAG = "SandboxContext";
 
@@ -126,7 +148,8 @@
         private boolean mDestroyed = false;
 
         public SandboxContext(Context base) {
-            super(base);
+            attachBaseContext(base);
+            initDagger();
         }
 
         @Override
@@ -134,7 +157,20 @@
             return this;
         }
 
+        @Override
+        public boolean shouldCleanUpOnDestroy() {
+            return (getBaseContext().getApplicationContext() instanceof SandboxApplication sa)
+                    ? sa.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/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..e51609a 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,7 +39,6 @@
 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;
@@ -89,69 +85,6 @@
     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;
-        }
-    }
-
-    /**
      * Returns the installing app package for the given package
      */
     public String getAppInstallerPackage(@NonNull final String packageName) {
@@ -164,20 +97,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 +116,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 +123,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 +147,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 +185,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/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 a4b8eb0..adb8f9d 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -31,8 +31,6 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.Utilities;
-
 /**
  * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
  */
@@ -129,7 +127,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/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/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/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index d3160e0..b8481c5 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -26,9 +26,11 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
+import android.app.Activity;
 import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
@@ -46,6 +48,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
+import android.view.Window;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
 import android.view.inputmethod.InputMethodManager;
@@ -76,10 +79,11 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.ActivityOptionsWrapper;
-import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.ViewCache;
 import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
@@ -175,6 +179,23 @@
     BaseDragLayer getDragLayer();
 
     /**
+     * @see Activity#getWindow()
+     * @return Window
+     */
+    @Nullable
+    default Window getWindow() {
+        return null;
+    }
+
+    /**
+     * @see Activity#getComponentName()
+     * @return ComponentName
+     */
+    default ComponentName getComponentName() {
+        return null;
+    }
+
+    /**
      * The all apps container, if it exists in this context.
      */
     default ActivityAllAppsContainerView<?> getAppsView() {
@@ -216,6 +237,11 @@
         return null;
     }
 
+    @Nullable
+    default SystemUiController getSystemUiController() {
+        return null;
+    }
+
     /**
      * Handler for actions taken on drop targets that require launcher
      */
@@ -391,7 +417,7 @@
             View v, Intent intent, @Nullable ItemInfo item) {
         Preconditions.assertUIThread();
         Context context = (Context) this;
-        if (isAppBlockedForSafeMode() && !PackageManagerHelper.isSystemApp(context, intent)) {
+        if (isAppBlockedForSafeMode() && !new ApplicationInfoWrapper(context, intent).isSystem()) {
             Toast.makeText(context, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
             return null;
         }
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 5d2d3f4..ea3fb3f 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -107,7 +107,7 @@
     protected final RectF mSystemGestureRegion = new RectF();
     private int mTouchDispatchState = 0;
 
-    protected final T mActivity;
+    protected final T mContainer;
     private final MultiValueAlpha mMultiValueAlpha;
 
     // All the touch controllers for the view
@@ -121,7 +121,7 @@
 
     public BaseDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
         super(context, attrs);
-        mActivity = ActivityContext.lookupContext(context);
+        mContainer = ActivityContext.lookupContext(context);
         mMultiValueAlpha = new MultiValueAlpha(this, alphaChannelCount);
     }
 
@@ -159,7 +159,7 @@
             }
             mTouchCompleteListener = null;
         } else if (action == MotionEvent.ACTION_DOWN) {
-            mActivity.finishAutoCancelActionMode();
+            mContainer.finishAutoCancelActionMode();
         }
         return findActiveController(ev);
     }
@@ -173,7 +173,7 @@
     }
 
     private TouchController findControllerToHandleTouch(MotionEvent ev) {
-        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (topView != null
                 && (isEventWithinSystemGestureRegion(ev)
                 || topView.canInterceptEventsInSystemGestureRegion())
@@ -207,7 +207,7 @@
     @Override
     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
         // Shortcuts can appear above folder
-        View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
+        View topView = AbstractFloatingView.getTopOpenViewWithType(mContainer,
                 AbstractFloatingView.TYPE_ACCESSIBLE);
         if (topView != null) {
             if (child == topView) {
@@ -222,7 +222,7 @@
 
     @Override
     public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
-        View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
+        View topView = AbstractFloatingView.getTopOpenViewWithType(mContainer,
                 AbstractFloatingView.TYPE_ACCESSIBLE);
         if (topView != null) {
             // Only add the top view as a child for accessibility when it is open
@@ -458,7 +458,7 @@
 
     @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
-        View topView = AbstractFloatingView.getTopOpenView(mActivity);
+        View topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (topView != null) {
             return topView.requestFocus(direction, previouslyFocusedRect);
         } else {
@@ -468,7 +468,7 @@
 
     @Override
     public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
-        View topView = AbstractFloatingView.getTopOpenView(mActivity);
+        View topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (topView != null) {
             topView.addFocusables(views, direction);
         } else {
@@ -555,7 +555,7 @@
         Insets gestureInsets = insets.getMandatorySystemGestureInsets();
         int gestureInsetBottom = gestureInsets.bottom;
         Insets imeInset = insets.getInsets(WindowInsets.Type.ime());
-        DeviceProfile dp = mActivity.getDeviceProfile();
+        DeviceProfile dp = mContainer.getDeviceProfile();
         if (dp.isTaskbarPresent) {
             // Ignore taskbar gesture insets to avoid interfering with TouchControllers.
             gestureInsetBottom = ResourceUtils.getNavbarSize(
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 4ee6aff..6739387 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -253,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) {
@@ -298,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();
             }
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 63648dd..6fd18be 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -355,7 +355,10 @@
         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;
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/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/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 856f4b3..12a14c2 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -104,7 +104,7 @@
 
     @UiThread
     private void enforceRoundedCorners() {
-        if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
+        if (mEnforcedCornerRadius <= 0) {
             resetRoundedCorners();
             return;
         }
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/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index 3e4fd8c..1db3b5a 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,6 +11,8 @@
 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;
@@ -116,15 +117,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 +134,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
@@ -213,8 +210,7 @@
     }
 
     public boolean isConfigurationOptional() {
-        return ATLEAST_S
-                && isReconfigurable()
+        return isReconfigurable()
                 && (getWidgetFeatures() & WIDGET_FEATURE_CONFIGURATION_OPTIONAL) != 0;
     }
 
@@ -230,6 +226,12 @@
 
     @Override
     public Drawable getFullResIcon(IconCache cache) {
-        return cache.getFullResIcon(provider.getPackageName(), icon);
+        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/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 2e5e251..cadaf89 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -28,8 +28,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;
@@ -71,11 +69,6 @@
         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,9 +95,6 @@
      * 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);
         float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius);
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_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java
index 4d7f937..63d87e8 100644
--- a/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java
+++ b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java
@@ -18,12 +18,10 @@
 
 import dagger.Component;
 
-import javax.inject.Singleton;
-
 /**
  * Root component for Dagger injection for Launcher AOSP.
  */
-@Singleton
+@LauncherAppSingleton
 @Component
 public interface LauncherAppComponent extends LauncherBaseAppComponent {
     /** Builder for aosp LauncherAppComponent. */
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 4b926a8..68e493d 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -183,6 +183,7 @@
         </activity>
         <activity-alias android:name="Activity2"
                         android:label="TestActivity2"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -192,6 +193,7 @@
         </activity-alias>
         <activity-alias android:name="Activity3"
                         android:label="TestActivity3"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -201,6 +203,7 @@
         </activity-alias>
         <activity-alias android:name="Activity4"
                         android:label="TestActivity4"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -210,6 +213,7 @@
         </activity-alias>
         <activity-alias android:name="Activity5"
                         android:label="TestActivity5"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -219,6 +223,7 @@
         </activity-alias>
         <activity-alias android:name="Activity6"
                         android:label="TestActivity6"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -228,6 +233,7 @@
         </activity-alias>
         <activity-alias android:name="Activity7"
                         android:label="TestActivity7"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -237,6 +243,7 @@
         </activity-alias>
         <activity-alias android:name="Activity8"
                         android:label="TestActivity8"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -246,6 +253,7 @@
         </activity-alias>
         <activity-alias android:name="Activity9" android:exported="true"
             android:label="TestActivity9"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -254,6 +262,7 @@
         </activity-alias>
         <activity-alias android:name="Activity10" android:exported="true"
             android:label="TestActivity10"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -262,6 +271,7 @@
         </activity-alias>
         <activity-alias android:name="Activity11" android:exported="true"
             android:label="TestActivity11"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -270,6 +280,7 @@
         </activity-alias>
         <activity-alias android:name="Activity12" android:exported="true"
             android:label="TestActivity12"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -278,6 +289,7 @@
         </activity-alias>
         <activity-alias android:name="Activity13" android:exported="true"
             android:label="TestActivity13"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -286,6 +298,7 @@
         </activity-alias>
         <activity-alias android:name="Activity14" android:exported="true"
             android:label="TestActivity14"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -363,7 +376,7 @@
         </activity>
         <activity android:name="com.android.launcher3.testcomponent.ImeTestActivity"
             android:label="ImeTestActivity"
-            android:icon="@drawable/test_theme_icon"
+            android:icon="@drawable/test_icon"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -407,6 +420,36 @@
             </intent-filter>
         </activity>
 
+        <activity-alias android:name="AppIconActivity"
+            android:label="Application Icon"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="DiffIconActivity"
+            android:label="Different icon"
+            android:exported="true"
+            android:icon="@drawable/test_different_activity_icon"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="WrongIconActivity"
+            android:label="Wrong icon"
+            android:exported="true"
+            android:icon="@drawable/test_wrong_activity_icon"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+
         <!-- Disable eager initialization of Jetpack libraries. See bug 197780098. -->
         <provider
             android:name="androidx.startup.InitializationProvider"
diff --git a/tests/assets/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 ea58136..b5932d5 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -168,9 +168,7 @@
     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 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/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/allapps/FloatingHeaderViewTests.kt b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt
similarity index 98%
rename from tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTests.kt
rename to tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt
index ac2c553..d2103ae 100644
--- a/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTests.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTest.kt
@@ -31,7 +31,7 @@
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
-class FloatingHeaderViewTests {
+class FloatingHeaderViewTest {
 
     @get:Rule val mSetFlagsRule = SetFlagsRule()
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
index d002493..371bac2 100644
--- a/tests/multivalentTests/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.BitmapInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.LauncherLayoutBuilder
@@ -58,7 +59,7 @@
             TEST_ACTIVITY11,
             TEST_ACTIVITY12,
             TEST_ACTIVITY13,
-            TEST_ACTIVITY14
+            TEST_ACTIVITY14,
         )
 
     @Before
@@ -169,6 +170,9 @@
             assertWithMessage("Index $index was not highRes")
                 .that(items[index].bitmap.isNullOrLowRes)
                 .isFalse()
+            assertWithMessage("Index $index was the default icon")
+                .that(isDefaultIcon(items[index].bitmap))
+                .isFalse()
         }
     }
 
@@ -177,9 +181,17 @@
             assertWithMessage("Index $index was not lowRes")
                 .that(items[index].bitmap.isNullOrLowRes)
                 .isTrue()
+            assertWithMessage("Index $index was the default icon")
+                .that(isDefaultIcon(items[index].bitmap))
+                .isFalse()
         }
     }
 
+    private fun isDefaultIcon(bitmap: BitmapInfo) =
+        LauncherAppState.getInstance(modelHelper.sandboxContext)
+            .iconCache
+            .isDefaultIcon(bitmap, modelHelper.sandboxContext.user)
+
     /** Recreate DeviceProfiles after changing InvariantDeviceProfile */
     private fun recreateSupportedDeviceProfiles() {
         LauncherAppState.getIDP(modelHelper.sandboxContext).supportedProfiles =
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
index 761f06d..f57e8a1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
@@ -45,7 +45,6 @@
 
     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 +67,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)
@@ -126,8 +111,8 @@
         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)
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
         GridSizeMigrationUtil.migrate(
             dbHelper,
             srcReader,
@@ -135,7 +120,7 @@
             idp.numDatabaseHotseatIcons,
             Point(idp.numColumns, idp.numRows),
             DeviceGridState(context),
-            DeviceGridState(idp)
+            DeviceGridState(idp),
         )
 
         // Check hotseat items
@@ -147,9 +132,8 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons)
 
@@ -178,9 +162,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         intentIndex = c.getColumnIndex(INTENT)
         val cellXIndex = c.getColumnIndex(CELLX)
@@ -238,8 +221,8 @@
         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,
@@ -248,7 +231,7 @@
             idp.numDatabaseHotseatIcons,
             Point(idp.numColumns, idp.numRows),
             DeviceGridState(context),
-            DeviceGridState(idp)
+            DeviceGridState(idp),
         )
 
         // Check hotseat items in grid B
@@ -260,15 +243,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 +262,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         var locMap = parseLocMap(c)
         // Expected items in grid B
         // _ _ _ _
@@ -306,7 +287,7 @@
             5,
             Point(5, 5),
             DeviceGridState(idp),
-            DeviceGridState(context)
+            DeviceGridState(context),
         )
         // Check hotseat items in grid A
         c =
@@ -317,15 +298,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 +317,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         locMap = parseLocMap(c)
         // Expected workspace items in grid A
         // _ _ _ _ _
@@ -367,7 +346,7 @@
             idp.numDatabaseHotseatIcons,
             Point(idp.numColumns, idp.numRows),
             DeviceGridState(context),
-            DeviceGridState(idp)
+            DeviceGridState(idp),
         )
 
         // Check hotseat items in grid B
@@ -379,15 +358,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 +377,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         locMap = parseLocMap(c)
         // Expected workspace items in grid B
         // _ _ _ _
@@ -455,7 +432,7 @@
                     0,
                     testPackage1,
                     1,
-                    TMP_TABLE
+                    TMP_TABLE,
                 ),
                 addItem(
                     ITEM_TYPE_DEEP_SHORTCUT,
@@ -465,7 +442,7 @@
                     0,
                     testPackage2,
                     2,
-                    TMP_TABLE
+                    TMP_TABLE,
                 ),
                 addItem(
                     ITEM_TYPE_APPLICATION,
@@ -475,7 +452,7 @@
                     0,
                     testPackage3,
                     3,
-                    TMP_TABLE
+                    TMP_TABLE,
                 ),
                 addItem(
                     ITEM_TYPE_DEEP_SHORTCUT,
@@ -485,15 +462,15 @@
                     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)
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
         GridSizeMigrationUtil.migrate(
             dbHelper,
             srcReader,
@@ -501,7 +478,7 @@
             idp.numDatabaseHotseatIcons,
             Point(idp.numColumns, idp.numRows),
             DeviceGridState(context),
-            DeviceGridState(idp)
+            DeviceGridState(idp),
         )
 
         // Check hotseat items
@@ -513,9 +490,8 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         assertThat(c.count.toLong()).isEqualTo(numSrcDatabaseHotseatIcons.toLong())
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -550,8 +526,8 @@
         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)
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
         GridSizeMigrationUtil.migrate(
             dbHelper,
             srcReader,
@@ -559,7 +535,7 @@
             idp.numDatabaseHotseatIcons,
             Point(idp.numColumns, idp.numRows),
             DeviceGridState(context),
-            DeviceGridState(idp)
+            DeviceGridState(idp),
         )
 
         // Check hotseat items
@@ -571,9 +547,8 @@
                 null,
                 SCREEN,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         assertThat(c.count.toLong()).isEqualTo(idp.numDatabaseHotseatIcons.toLong())
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -617,8 +592,8 @@
         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)
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
         GridSizeMigrationUtil.migrate(
             dbHelper,
             srcReader,
@@ -626,7 +601,7 @@
             idp.numDatabaseHotseatIcons,
             Point(idp.numColumns, idp.numRows),
             DeviceGridState(context),
-            DeviceGridState(idp)
+            DeviceGridState(idp),
         )
 
         // Get workspace items
@@ -638,9 +613,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
 
         val intentIndex = c.getColumnIndex(INTENT)
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -678,8 +652,8 @@
         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)
+        val srcReader = DbReader(db, TMP_TABLE, context)
+        val destReader = DbReader(db, TABLE_NAME, context)
         GridSizeMigrationUtil.migrate(
             dbHelper,
             srcReader,
@@ -687,7 +661,7 @@
             idp.numDatabaseHotseatIcons,
             Point(idp.numColumns, idp.numRows),
             DeviceGridState(context),
-            DeviceGridState(idp)
+            DeviceGridState(idp),
         )
 
         // Get workspace items
@@ -699,9 +673,8 @@
                 null,
                 null,
                 null,
-                null
-            )
-                ?: throw IllegalStateException()
+                null,
+            ) ?: throw IllegalStateException()
         val intentIndex = c.getColumnIndex(INTENT)
         val screenIndex = c.getColumnIndex(SCREEN)
 
@@ -732,7 +705,7 @@
         container: Int,
         x: Int,
         y: Int,
-        packageName: String?
+        packageName: String?,
     ): Int {
         return addItem(
             type,
@@ -742,7 +715,7 @@
             y,
             packageName,
             dbHelper.generateNewItemId(),
-            TABLE_NAME
+            TABLE_NAME,
         )
     }
 
@@ -754,7 +727,7 @@
         y: Int,
         packageName: String?,
         id: Int,
-        tableName: String
+        tableName: String,
     ): Int {
         val values = ContentValues()
         values.put(_ID, id)
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index 1d9c161..c7abce6 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -40,6 +40,7 @@
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_INFO
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_WIDGET_PROVIDER
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.PROFILE_DELETED
+import com.android.launcher3.icons.CacheableShortcutInfo
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.ItemInfo
@@ -97,7 +98,7 @@
     private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
     private var mKeyToPinnedShortcutsMap: MutableMap<ShortcutKey, ShortcutInfo> = mutableMapOf()
     private var mInstallingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = hashMapOf()
-    private var mAllDeepShortcuts: MutableList<ShortcutInfo> = mutableListOf()
+    private var mAllDeepShortcuts: MutableList<CacheableShortcutInfo> = mutableListOf()
     private var mWidgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> =
         mutableMapOf()
     private var mPendingPackages: MutableSet<PackageUserKey> = mutableSetOf()
@@ -118,11 +119,17 @@
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
+        mockLauncherApps =
+            mock<LauncherApps>().apply {
+                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
+            }
         mockContext =
             mock<Context>().apply {
                 whenever(packageManager).thenReturn(mock())
                 whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
                 whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
+                whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
             }
         mockAppState =
             mock<LauncherAppState>().apply {
@@ -135,11 +142,6 @@
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
                     .thenReturn(intent)
             }
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
-            }
         mockCursor =
             mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
                 user = mUserHandle
@@ -193,7 +195,7 @@
         pendingPackages: MutableSet<PackageUserKey> = mPendingPackages,
         unlockedUsers: LongSparseArray<Boolean> = mUnlockedUsersArray,
         installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = mInstallingPkgs,
-        allDeepShortcuts: MutableList<ShortcutInfo> = mAllDeepShortcuts
+        allDeepShortcuts: MutableList<CacheableShortcutInfo> = mAllDeepShortcuts,
     ) =
         WorkspaceItemProcessor(
             c = cursor,
@@ -212,7 +214,7 @@
             isSdCardReady = isSdCardReady,
             shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
             installingPkgs = installingPkgs,
-            allDeepShortcuts = allDeepShortcuts
+            allDeepShortcuts = allDeepShortcuts,
         )
 
     @Test
@@ -351,7 +353,7 @@
                     " targetPkg=package," +
                     " component=ComponentInfo{package/class}." +
                     " Unable to create launch Intent.",
-                MISSING_INFO
+                MISSING_INFO,
             )
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -386,7 +388,8 @@
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
         assertThat(mIconRequestInfos).isEmpty()
-        assertThat(mAllDeepShortcuts).containsExactly(expectedShortcutInfo)
+        assertThat(mAllDeepShortcuts.size).isEqualTo(1)
+        assertThat(mAllDeepShortcuts[0].shortcutInfo).isEqualTo(expectedShortcutInfo)
         verify(mockCursor).markRestored()
         verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -412,7 +415,7 @@
         verify(mockCursor)
             .markDeleted(
                 "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
-                "shortcut_not_found"
+                "shortcut_not_found",
             )
     }
 
@@ -451,7 +454,8 @@
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
         assertThat(mIconRequestInfos).isEmpty()
-        assertThat(mAllDeepShortcuts).containsExactly(expectedShortcutInfo)
+        assertThat(mAllDeepShortcuts.size).isEqualTo(1)
+        assertThat(mAllDeepShortcuts[0].shortcutInfo).isEqualTo(expectedShortcutInfo)
         verify(mockCursor).markRestored()
         verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -549,7 +553,7 @@
         val inflationResult =
             WidgetInflater.InflationResult(
                 type = WidgetInflater.TYPE_REAL,
-                widgetInfo = expectedWidgetProviderInfo
+                widgetInfo = expectedWidgetProviderInfo,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
@@ -607,7 +611,7 @@
         val inflationResult =
             WidgetInflater.InflationResult(
                 type = WidgetInflater.TYPE_PENDING,
-                widgetInfo = mockProviderInfo
+                widgetInfo = mockProviderInfo,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
@@ -662,7 +666,7 @@
         verify(mockCursor)
             .markDeleted(
                 "processWidget: Unrestored Pending widget removed: id=1, appWidgetId=0, component=$expectedComponentName, restoreFlag:=4",
-                LauncherRestoreEventLogger.RestoreError.APP_NOT_INSTALLED
+                LauncherRestoreEventLogger.RestoreError.APP_NOT_INSTALLED,
             )
     }
 
@@ -670,12 +674,6 @@
     fun `When widget inflation result is TYPE_DELETE then mark deleted`() {
         // Given
         val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
-        val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
-        val expectedPackage = expectedComponentName!!.packageName
-        mockPmHelper =
-            mock<PackageManagerHelper>().apply {
-                whenever(isAppArchived(expectedPackage)).thenReturn(true)
-            }
         mockCursor =
             mock<LoaderCursor>().apply {
                 itemType = ITEM_TYPE_APPWIDGET
@@ -694,7 +692,7 @@
                 type = WidgetInflater.TYPE_DELETE,
                 widgetInfo = null,
                 reason = "test_delete_reason",
-                restoreErrorType = MISSING_WIDGET_PROVIDER
+                restoreErrorType = MISSING_WIDGET_PROVIDER,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
diff --git a/tests/multivalentTests/src/com/android/launcher3/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/DisplayControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 41effa2..308f200 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,24 @@
     @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())
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
index 2d53e29..748d376 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;
@@ -185,6 +186,8 @@
      */
     public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
             throws Exception {
+        grantWriteSecurePermission();
+
         InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext);
         if (idp.numRows == 0 && idp.numColumns == 0) {
             idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
@@ -283,11 +286,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/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/SystemUiControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
index 612fcd4..043bdac 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
@@ -52,7 +52,7 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
         `when`(window.decorView).thenReturn(decorView)
-        underTest = SystemUiController(window)
+        underTest = SystemUiController(window.decorView)
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
index 3646f0c..64035da 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
@@ -26,6 +26,7 @@
 
 import static org.junit.Assert.assertTrue;
 
+import android.Manifest;
 import android.app.Instrumentation;
 import android.app.blob.BlobHandle;
 import android.app.blob.BlobStoreManager;
@@ -169,6 +170,8 @@
         }
 
         String key = Base64.encodeToString(digest, NO_WRAP | NO_PADDING);
+
+        grantWriteSecurePermission();
         Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, key);
         wait.await();
         return () ->
@@ -224,6 +227,14 @@
         assertTrue(message, failed);
     }
 
+    /**
+     * Grants [WRITE_SECURE_SETTINGS] permission in runtime.
+     */
+    public static void grantWriteSecurePermission() {
+        getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.WRITE_SECURE_SETTINGS);
+    }
+
     /** 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/rule/TestStabilityRule.java b/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index ad2d8c2..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]+)"
                     + ")$");
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/util/WidgetsTableUtilsTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
index b2cb266..7adb2b1 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
@@ -296,7 +296,7 @@
     private final class TestShortcutConfigActivityInfo extends ShortcutConfigActivityInfo {
 
         TestShortcutConfigActivityInfo(ComponentName componentName, UserHandle user) {
-            super(componentName, user);
+            super(componentName, user, mContext);
         }
 
         @Override
diff --git a/tests/res/drawable/test_app_info_icon.xml b/tests/res/drawable/test_app_info_icon.xml
new file mode 100644
index 0000000..2e824ac
--- /dev/null
+++ b/tests/res/drawable/test_app_info_icon.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2024 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#FFFF0000" />
+    </foreground>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_different_activity_icon.xml b/tests/res/drawable/test_different_activity_icon.xml
new file mode 100644
index 0000000..43d3611
--- /dev/null
+++ b/tests/res/drawable/test_different_activity_icon.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2024 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#FFFFFF00" />
+    </foreground>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_icon.xml b/tests/res/drawable/test_icon.xml
new file mode 100644
index 0000000..72ebfeb
--- /dev/null
+++ b/tests/res/drawable/test_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2024 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#FFFF0000" />
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</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/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
index 479b201..35ac0a1 100644
--- a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
+++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
@@ -23,6 +23,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
@@ -67,4 +68,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/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 76c1948..59e1f99 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;
@@ -197,7 +196,6 @@
     @PlatinumTest(focusArea = "launcher")
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/343953783
     public void testDragAppIcon() {
 
         final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
@@ -230,11 +228,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/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/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index d16674c..0dd13a9 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -22,6 +22,7 @@
 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
@@ -116,6 +117,8 @@
         `when`(idleLock.awaitLocked(1000)).thenReturn(false)
         `when`(iconCache.updateHandler).thenReturn(iconCacheUpdateHandler)
         context.putObject(UserCache.INSTANCE, userCache)
+
+        TestUtil.grantWriteSecurePermission()
     }
 
     @After
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
index b93c305..d553f47 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.content.pm.ApplicationInfo
 import android.content.pm.LauncherApps
 import android.content.pm.PackageInstaller
 import android.content.pm.ShortcutInfo
@@ -28,14 +29,13 @@
 import android.util.LongSparseArray
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.dx.mockito.inline.extended.ExtendedMockito
 import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
-import com.android.launcher3.Utilities
+import com.android.launcher3.icons.CacheableShortcutInfo
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
@@ -48,7 +48,6 @@
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.UserIconInfo
 import com.android.launcher3.widget.WidgetInflater
-import com.android.launcher3.widget.WidgetSections
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -63,7 +62,6 @@
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
-import org.mockito.quality.Strictness
 
 @RunWith(AndroidJUnit4::class)
 class WorkspaceItemProcessorExtraTest {
@@ -87,7 +85,7 @@
     private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
     private var mKeyToPinnedShortcutsMap: MutableMap<ShortcutKey, ShortcutInfo> = mutableMapOf()
     private var mInstallingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = hashMapOf()
-    private var mAllDeepShortcuts: MutableList<ShortcutInfo> = mutableListOf()
+    private var mAllDeepShortcuts: MutableList<CacheableShortcutInfo> = mutableListOf()
     private var mWidgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> =
         mutableMapOf()
     private var mPendingPackages: MutableSet<PackageUserKey> = mutableSetOf()
@@ -108,11 +106,17 @@
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
+        mockLauncherApps =
+            mock<LauncherApps>().apply {
+                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
+            }
         mockContext =
             mock<Context>().apply {
                 whenever(packageManager).thenReturn(mock())
                 whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
                 whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
+                whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
             }
         mockAppState =
             mock<LauncherAppState>().apply {
@@ -125,11 +129,6 @@
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
                     .thenReturn(intent)
             }
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
-            }
         mockCursor =
             Mockito.mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
                 user = mUserHandle
@@ -163,138 +162,116 @@
 
     @Test
     fun `When Pending App Widget has not started restore then update db and add item`() {
-
-        val mockitoSession =
-            ExtendedMockito.mockitoSession()
-                .strictness(Strictness.LENIENT)
-                .mockStatic(WidgetSections::class.java)
-                .startMocking()
-        try {
-            // Given
-            val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
-            val expectedComponentName =
-                ComponentName.unflattenFromString(expectedProvider)!!.flattenToString()
-            val expectedRestoreStatus = FLAG_UI_NOT_READY or FLAG_RESTORE_STARTED
-            val expectedAppWidgetId = 0
-            mockCursor.apply {
-                itemType = ITEM_TYPE_APPWIDGET
-                user = mUserHandle
-                restoreFlag = FLAG_UI_NOT_READY
-                container = CONTAINER_DESKTOP
-                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
-                whenever(appWidgetProvider).thenReturn(expectedProvider)
-                whenever(appWidgetId).thenReturn(expectedAppWidgetId)
-                whenever(spanX).thenReturn(2)
-                whenever(spanY).thenReturn(1)
-                whenever(options).thenReturn(0)
-                whenever(appWidgetSource).thenReturn(20)
-                whenever(applyCommonProperties(any())).thenCallRealMethod()
-                whenever(
-                        updater()
-                            .put(Favorites.APPWIDGET_PROVIDER, expectedComponentName)
-                            .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
-                            .put(Favorites.RESTORED, expectedRestoreStatus)
-                            .commit()
-                    )
-                    .thenReturn(1)
-            }
-            val inflationResult =
-                WidgetInflater.InflationResult(
-                    type = WidgetInflater.TYPE_PENDING,
-                    widgetInfo = null
-                )
-            mockWidgetInflater =
-                mock<WidgetInflater>().apply {
-                    whenever(inflateAppWidget(any())).thenReturn(inflationResult)
-                }
-            val packageUserKey = PackageUserKey("com.google.android.testApp", mUserHandle)
-            mInstallingPkgs[packageUserKey] = PackageInstaller.SessionInfo()
-
-            // When
-            itemProcessorUnderTest =
-                createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
-            itemProcessorUnderTest.processItem()
-
-            // Then
-            val expectedWidgetInfo =
-                LauncherAppWidgetInfo().apply {
-                    appWidgetId = expectedAppWidgetId
-                    providerName = ComponentName.unflattenFromString(expectedProvider)
-                    restoreStatus = expectedRestoreStatus
-                }
-            verify(
-                    mockCursor
-                        .updater()
-                        .put(Favorites.APPWIDGET_PROVIDER, expectedProvider)
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        val expectedComponentName =
+            ComponentName.unflattenFromString(expectedProvider)!!.flattenToString()
+        val expectedRestoreStatus = FLAG_UI_NOT_READY or FLAG_RESTORE_STARTED
+        val expectedAppWidgetId = 0
+        mockCursor.apply {
+            itemType = ITEM_TYPE_APPWIDGET
+            user = mUserHandle
+            restoreFlag = FLAG_UI_NOT_READY
+            container = CONTAINER_DESKTOP
+            whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+            whenever(appWidgetProvider).thenReturn(expectedProvider)
+            whenever(appWidgetId).thenReturn(expectedAppWidgetId)
+            whenever(spanX).thenReturn(2)
+            whenever(spanY).thenReturn(1)
+            whenever(options).thenReturn(0)
+            whenever(appWidgetSource).thenReturn(20)
+            whenever(applyCommonProperties(any())).thenCallRealMethod()
+            whenever(
+                    updater()
+                        .put(Favorites.APPWIDGET_PROVIDER, expectedComponentName)
                         .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
                         .put(Favorites.RESTORED, expectedRestoreStatus)
+                        .commit()
                 )
-                .commit()
-            val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
-            verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
-            val actualWidgetInfo = widgetInfoCaptor.value
-            with(actualWidgetInfo) {
-                assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
-                assertThat(restoreStatus).isEqualTo(expectedWidgetInfo.restoreStatus)
-                assertThat(targetComponent).isEqualTo(expectedWidgetInfo.targetComponent)
-                assertThat(appWidgetId).isEqualTo(expectedWidgetInfo.appWidgetId)
+                .thenReturn(1)
+        }
+        val inflationResult =
+            WidgetInflater.InflationResult(type = WidgetInflater.TYPE_PENDING, widgetInfo = null)
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
             }
-        } finally {
-            mockitoSession.finishMocking()
+        val packageUserKey = PackageUserKey("com.google.android.testApp", mUserHandle)
+        mInstallingPkgs[packageUserKey] = PackageInstaller.SessionInfo()
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        val expectedWidgetInfo =
+            LauncherAppWidgetInfo().apply {
+                appWidgetId = expectedAppWidgetId
+                providerName = ComponentName.unflattenFromString(expectedProvider)
+                restoreStatus = expectedRestoreStatus
+            }
+        verify(
+                mockCursor
+                    .updater()
+                    .put(Favorites.APPWIDGET_PROVIDER, expectedProvider)
+                    .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
+                    .put(Favorites.RESTORED, expectedRestoreStatus)
+            )
+            .commit()
+        val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
+        verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
+        val actualWidgetInfo = widgetInfoCaptor.value
+        with(actualWidgetInfo) {
+            assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
+            assertThat(restoreStatus).isEqualTo(expectedWidgetInfo.restoreStatus)
+            assertThat(targetComponent).isEqualTo(expectedWidgetInfo.targetComponent)
+            assertThat(appWidgetId).isEqualTo(expectedWidgetInfo.appWidgetId)
         }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
     fun `When Archived Pending App Widget then checkAndAddItem`() {
-        val mockitoSession =
-            ExtendedMockito.mockitoSession().mockStatic(Utilities::class.java).startMocking()
-        try {
-            // Given
-            val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
-            val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
-            val expectedPackage = expectedComponentName!!.packageName
-            mockPmHelper =
-                mock<PackageManagerHelper>().apply {
-                    whenever(isAppArchived(expectedPackage)).thenReturn(true)
-                }
-            mockCursor =
-                mock<LoaderCursor>().apply {
-                    itemType = ITEM_TYPE_APPWIDGET
-                    id = 1
-                    user = UserHandle(1)
-                    restoreFlag = FLAG_UI_NOT_READY
-                    container = CONTAINER_DESKTOP
-                    whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
-                    whenever(appWidgetProvider).thenReturn(expectedProvider)
-                    whenever(appWidgetId).thenReturn(0)
-                    whenever(spanX).thenReturn(2)
-                    whenever(spanY).thenReturn(1)
-                    whenever(options).thenReturn(0)
-                    whenever(appWidgetSource).thenReturn(20)
-                    whenever(applyCommonProperties(any())).thenCallRealMethod()
-                }
-            mInstallingPkgs = hashMapOf()
-            val inflationResult =
-                WidgetInflater.InflationResult(
-                    type = WidgetInflater.TYPE_PENDING,
-                    widgetInfo = null
-                )
-            mockWidgetInflater =
-                mock<WidgetInflater>().apply {
-                    whenever(inflateAppWidget(any())).thenReturn(inflationResult)
-                }
-            itemProcessorUnderTest =
-                createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
+        val expectedPackage = expectedComponentName!!.packageName
+        val expectedUser = UserHandle(1)
 
-            // When
-            itemProcessorUnderTest.processItem()
+        whenever(mockLauncherApps.getApplicationInfo(eq(expectedPackage), any(), eq(expectedUser)))
+            .thenReturn(ApplicationInfo().apply { isArchived = true })
+        mockCursor =
+            mock<LoaderCursor>().apply {
+                itemType = ITEM_TYPE_APPWIDGET
+                id = 1
+                user = expectedUser
+                restoreFlag = FLAG_UI_NOT_READY
+                container = CONTAINER_DESKTOP
+                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+                whenever(appWidgetProvider).thenReturn(expectedProvider)
+                whenever(appWidgetId).thenReturn(0)
+                whenever(spanX).thenReturn(2)
+                whenever(spanY).thenReturn(1)
+                whenever(options).thenReturn(0)
+                whenever(appWidgetSource).thenReturn(20)
+                whenever(applyCommonProperties(any())).thenCallRealMethod()
+            }
+        mInstallingPkgs = hashMapOf()
+        val inflationResult =
+            WidgetInflater.InflationResult(type = WidgetInflater.TYPE_PENDING, widgetInfo = null)
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+            }
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
 
-            // Then
-            verify(mockCursor).checkAndAddItem(any(), any())
-        } finally {
-            mockitoSession.finishMocking()
-        }
+        // When
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        verify(mockCursor).checkAndAddItem(any(), any())
     }
 
     private fun createWorkspaceItemProcessorUnderTest(
@@ -314,7 +291,7 @@
         pendingPackages: MutableSet<PackageUserKey> = mPendingPackages,
         unlockedUsers: LongSparseArray<Boolean> = mUnlockedUsersArray,
         installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = mInstallingPkgs,
-        allDeepShortcuts: MutableList<ShortcutInfo> = mAllDeepShortcuts
+        allDeepShortcuts: MutableList<CacheableShortcutInfo> = mAllDeepShortcuts,
     ) =
         WorkspaceItemProcessor(
             c = cursor,
@@ -333,6 +310,6 @@
             isSdCardReady = isSdCardReady,
             shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
             installingPkgs = installingPkgs,
-            allDeepShortcuts = allDeepShortcuts
+            allDeepShortcuts = allDeepShortcuts,
         )
 }
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
index 7182cf3..03d0195 100644
--- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -114,17 +114,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)
@@ -135,12 +132,12 @@
         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)),
+                GridSizeMigrationUtil.DbReader(it.db, srcGrid.tableName, context),
+                GridSizeMigrationUtil.DbReader(it.db, dstGrid.tableName, context),
                 dstGrid.size.x,
                 dstGrid.size,
                 srcGrid.toGridState(),
-                dstGrid.toGridState()
+                dstGrid.toGridState(),
             )
             it.commit()
         }
@@ -157,7 +154,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())
@@ -175,13 +172,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))
         }
@@ -199,7 +196,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/pm/InstallSessionHelperTest.kt b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
index 3dd8dbc..ca2ef42 100644
--- a/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
+++ b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.pm.ApplicationInfo
 import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.ApplicationInfo.FLAG_SYSTEM
 import android.content.pm.LauncherApps
 import android.content.pm.PackageInstaller
 import android.content.pm.PackageManager
@@ -35,7 +36,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
@@ -126,13 +129,10 @@
     fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
         // Given
         val expectedApplicationInfo =
-            ApplicationInfo().apply {
-                flags = flags or FLAG_INSTALLED
-                enabled = true
-            }
+            ApplicationInfo().apply { flags = FLAG_SYSTEM or FLAG_INSTALLED }
         doReturn(expectedApplicationInfo)
             .whenever(launcherApps)
-            .getApplicationInfo(expectedAppPackage, ApplicationInfo.FLAG_SYSTEM, UserHandle(0))
+            .getApplicationInfo(eq(expectedAppPackage), any(), eq(UserHandle(0)))
         // When
         val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
         // Then
diff --git a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
index dcfcad5..f54668c 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,13 @@
 
 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.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 +73,9 @@
 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;
 
@@ -72,6 +85,7 @@
 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;
 
@@ -88,25 +102,31 @@
     private PrivateProfileManager mPrivateProfileManager;
     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.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,7 +134,6 @@
         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);
@@ -168,6 +187,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_ENABLE_DISMISS_PREDICTION_UNDO)
     public void testDontSuggestAppForPredictedItem() {
         mAppInfo = new AppInfo();
         mAppInfo.componentName = new ComponentName(mTestContext, getClass());
@@ -176,7 +196,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
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/TaplWorkProfileTest.java b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
index b2e413d..b38dd4b 100644
--- a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
@@ -45,7 +45,6 @@
 import com.android.launcher3.allapps.WorkProfileManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 
 import org.junit.After;
@@ -147,7 +146,6 @@
 
     // Staging; will be promoted to presubmit if stable
     @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
-    @ScreenRecord
     @Test
     public void toggleWorks() {
         assumeTrue(mWorkProfileSetupSuccessful);
@@ -195,7 +193,6 @@
 
     }
 
-    @ScreenRecord // b/322823478
     @Test
     public void testEdu() {
         assumeTrue(mWorkProfileSetupSuccessful);
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index 20c5a25..2edd129 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -164,7 +164,6 @@
 
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/352130094
     public void testDragIconToPage2() {
         Workspace workspace = mLauncher.getWorkspace();
 
@@ -242,7 +241,6 @@
         });
     }
 
-    @ScreenRecordRule.ScreenRecord // b/329935119
     @Test
     @PortraitLandscape
     public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() {
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/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 02a862d..9294755 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,10 +16,8 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
 
 import android.graphics.Point;
-import android.util.Log;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -99,8 +97,6 @@
 
     @Override
     protected void waitForLongPressConfirmation() {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "AppIcon.waitForLongPressConfirmation, resName: popupContainer");
         mLauncher.waitForLauncherObject("popup_container");
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index b7ebfcd..e1bd686 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -123,10 +123,10 @@
                                     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));
+                                    "Task(s) found to the right of the swiped task",
+                                    tasks.stream().allMatch(t ->
+                                            t.getVisibleBounds().right < centerX
+                                                    || t.getVisibleBounds().centerX() == centerX));
                         }
 
                     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 9d3bc6e..c40e5a9 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -17,10 +17,8 @@
 package com.android.launcher3.tapl;
 
 import static com.android.launcher3.testing.shared.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
 
 import android.graphics.Point;
-import android.util.Log;
 import android.view.MotionEvent;
 
 import androidx.test.uiautomator.UiObject2;
@@ -115,10 +113,6 @@
                 iconCenter.y - getStartDragThreshold());
 
         if (runToSpringLoadedState) {
-            Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                    "Launchable.startDrag: actionName: long-pressing and triggering drag start"
-                            + " iconCenter: " + iconCenter + " dragStartCenter: "
-                            + dragStartCenter);
             mLauncher.runToState(() -> movePointerForStartDrag(
                             downTime,
                             iconCenter,
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index d3c423e..08c5552 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;
@@ -524,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();
@@ -542,6 +545,8 @@
             }
         } catch (IOException e) {
             Log.e(TAG, "error dumping XML to logcat", e);
+        } finally {
+            Trace.endSection();
         }
     }
 
@@ -621,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();
         }
     }
 
@@ -1005,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() {
@@ -1197,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,
@@ -1259,8 +1268,13 @@
     }
 
     boolean isLauncherVisible() {
-        mDevice.waitForIdle();
-        return hasLauncherObject(getAnyObjectSelector());
+        try {
+            Trace.beginSection("isLauncherVisible");
+            mDevice.waitForIdle();
+            return hasLauncherObject(getAnyObjectSelector());
+        } finally {
+            Trace.endSection();
+        }
     }
 
     boolean isLauncherContainerVisible() {
@@ -1547,8 +1561,6 @@
 
     @NonNull
     UiObject2 waitForLauncherObject(String resName) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForLauncherObject");
         return waitForObjectBySelector(getLauncherObjectSelector(resName));
     }
 
@@ -1578,16 +1590,12 @@
 
     @NonNull
     List<UiObject2> waitForObjectsBySelector(BySelector selector) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForObjectsBySelector");
         final List<UiObject2> objects = mDevice.wait(Until.findObjects(selector), WAIT_TIME_MS);
         assertNotNull("Can't find any view in Launcher, selector: " + selector, objects);
         return objects;
     }
 
     UiObject2 waitForObjectBySelector(BySelector selector) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForObjectBySelector");
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
         return object;
@@ -1630,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();
@@ -2032,15 +2037,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++;
@@ -2403,7 +2404,7 @@
         eventChecker.setLogExclusionRule(event -> {
             Matcher matcher = Pattern.compile("KeyEvent.*flags=0x([0-9a-fA-F]+)").matcher(event);
             if (matcher.find()) {
-                int keyEventFlags = Integer.parseInt(matcher.group(1), 16);
+                long keyEventFlags = Long.parseLong(matcher.group(1), 16);
                 // ignore KeyEvents with FLAG_CANCELED
                 return (keyEventFlags & KeyEvent.FLAG_CANCELED) != 0;
             }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 5433fa7..9a8d952 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -282,7 +282,7 @@
      * 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,
             OverviewSplitTask overviewSplitTask) {
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 748d576..c8c5384 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -25,7 +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;
@@ -646,8 +645,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,