Merge "Show Taskbar education when launching an app pair." into main
diff --git a/Android.bp b/Android.bp
index e358005..ba04bb3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -42,6 +42,24 @@
     ],
 }
 
+// Main Launcher source for compose, excluding the build config
+filegroup {
+    name: "launcher-compose-enabled-src",
+    srcs: [
+        "compose/facade/enabled/*.kt",
+        "compose/facade/core/*.kt",
+        "compose/features/**/*.kt",
+    ],
+}
+
+filegroup {
+    name: "launcher-compose-disabled-src",
+    srcs: [
+        "compose/facade/core/*.kt",
+        "compose/facade/disabled/*.kt",
+    ],
+}
+
 // Source code for quickstep build, on top of launcher-src
 filegroup {
     name: "launcher-quickstep-src",
@@ -51,6 +69,24 @@
     ],
 }
 
+// Source code for quickstep build with compose enabled, on top of launcher-src
+filegroup {
+    name: "launcher-quickstep-compose-enabled-src",
+    srcs: [
+        "quickstep/compose/facade/core/*.kt",
+        "quickstep/compose/facade/enabled/*.kt",
+        "quickstep/compose/features/**/*.kt",
+    ],
+}
+
+filegroup {
+    name: "launcher-quickstep-compose-disabled-src",
+    srcs: [
+        "quickstep/compose/facade/core/*.kt",
+        "quickstep/compose/facade/disabled/*.kt",
+    ],
+}
+
 // Alternate source when quickstep is not included
 filegroup {
     name: "launcher-src_no_quickstep",
@@ -74,6 +110,114 @@
     srcs: ["proguard.flags"],
 }
 
+// Opt-in configuration for Launcher3 code depending on Jetpack Compose.
+soong_config_module_type {
+    name: "launcher_compose_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: ["release_enable_compose_in_launcher"],
+    properties: [
+        "srcs",
+        "static_libs",
+    ],
+}
+
+// Opt-in configuration for Launcher Quickstep code depending on Jetpack Compose.
+soong_config_bool_variable {
+    name: "release_enable_compose_in_launcher",
+}
+
+soong_config_module_type {
+    name: "quickstep_compose_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: ["release_enable_compose_in_launcher"],
+    properties: [
+        "srcs",
+        "static_libs",
+    ],
+}
+
+soong_config_module_type {
+    name: "launcher_compose_tests_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: ["release_enable_compose_in_launcher"],
+    properties: [
+        "static_libs",
+    ],
+}
+
+launcher_compose_java_defaults {
+    name: "launcher_compose_defaults",
+    soong_config_variables: {
+        release_enable_compose_in_launcher: {
+            srcs: [
+                ":launcher-compose-enabled-src"
+            ],
+
+            // Compose dependencies
+            static_libs: [
+                "androidx.compose.runtime_runtime",
+                "androidx.compose.material3_material3",
+            ],
+
+            // By default, Compose is disabled and we compile the ComposeFacade
+            // in compose/launcher3/facade/disabled/.
+            conditions_default: {
+                srcs: [
+                    ":launcher-compose-disabled-src"
+                ],
+                static_libs: [],
+            },
+        },
+    },
+}
+
+quickstep_compose_java_defaults {
+    name: "quickstep_compose_defaults",
+    soong_config_variables: {
+        release_enable_compose_in_launcher: {
+            srcs: [
+                ":launcher-quickstep-compose-enabled-src"
+            ],
+
+            // Compose dependencies
+            static_libs: [
+                "androidx.compose.runtime_runtime",
+                "androidx.compose.material3_material3",
+            ],
+
+            // By default, Compose is disabled and we compile the ComposeFacade
+            // in compose/quickstep/facade/disabled/.
+            conditions_default: {
+                srcs: [
+                    ":launcher-quickstep-compose-disabled-src"
+                ],
+                static_libs: [],
+            },
+        },
+    },
+}
+
+launcher_compose_tests_java_defaults {
+    name: "launcher_compose_tests_defaults",
+    soong_config_variables: {
+        release_enable_compose_in_launcher: {
+            // Compose dependencies
+            static_libs: [
+                "androidx.compose.runtime_runtime",
+                "androidx.compose.ui_ui-test-junit4",
+                "androidx.compose.ui_ui-test-manifest",
+            ],
+
+            conditions_default: {
+                static_libs: [],
+            },
+        },
+    },
+}
+
 android_library {
     name: "launcher-aosp-tapl",
     libs: [
@@ -147,6 +291,9 @@
 // Library with all the dependencies for building Launcher3
 android_library {
     name: "Launcher3ResLib",
+    defaults: [
+        "launcher_compose_defaults",
+    ],
     srcs: [],
     resource_dirs: ["res"],
     static_libs: [
@@ -173,6 +320,7 @@
         "kotlinx_coroutines",
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
+
     ],
     manifest: "AndroidManifest-common.xml",
     sdk_version: "current",
@@ -250,6 +398,10 @@
 // Library with all the source code and dependencies for building Launcher Go
 android_library {
     name: "Launcher3GoLib",
+    defaults: [
+        "launcher_compose_defaults",
+        "quickstep_compose_defaults",
+    ],
     srcs: [
         ":launcher-src",
         ":launcher-quickstep-src",
@@ -281,6 +433,10 @@
 // Library with all the source code and dependencies for building Quickstep
 android_library {
     name: "Launcher3QuickStepLib",
+    defaults: [
+        "launcher_compose_defaults",
+        "quickstep_compose_defaults"
+    ],
     srcs: [
         ":launcher-src",
         ":launcher-quickstep-src",
@@ -308,7 +464,6 @@
 // Build rule for Quickstep app.
 android_app {
     name: "Launcher3QuickStep",
-
     static_libs: ["Launcher3QuickStepLib"],
     optimize: {
         proguard_flags_files: [":launcher-proguard-rules"],
@@ -349,7 +504,6 @@
 // eventually be merged into a single target
 android_app {
     name: "Launcher3Go",
-
     static_libs: ["Launcher3GoLib"],
     resource_dirs: [],
 
@@ -386,7 +540,6 @@
 }
 android_app {
     name: "Launcher3QuickStepGo",
-
     static_libs: ["Launcher3GoLib"],
     resource_dirs: [],
 
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index a779641..0df6c36 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -311,13 +311,6 @@
 }
 
 flag {
-    name: "enable_new_archiving_icon"
-    namespace: "launcher"
-    description: "Archived apps will use new icon in app title"
-    bug: "350758155"
-}
-
-flag {
     name: "enable_multi_instance_menu_taskbar"
     namespace: "launcher"
     description: "Menu in Taskbar with options to launch and manage multiple instances of the same app"
@@ -329,3 +322,13 @@
     description: "Settings screen supports navigating to child preference if the key is not on the screen"
     bug: "293390881"
 }
+
+flag {
+    name: "use_new_icon_for_archived_apps"
+    namespace: "launcher"
+    description: "Archived apps will use new cloud icon in app title instead of overlay"
+    bug: "350758155"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index f9327fe..11740ee 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -21,3 +21,13 @@
     description: "Enables rewritten version of TaskThumbnailViews in Overview"
     bug: "331753115"
 }
+
+flag {
+    name: "enable_hover_of_child_elements_in_taskview"
+    namespace: "launcher_overview"
+    description: "Enables child elements to receive hover events in TaskView and respond visually to those hover events."
+    bug: "342594235"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/compose/facade/core/BaseComposeFacade.kt b/compose/facade/core/BaseComposeFacade.kt
new file mode 100644
index 0000000..bc7ba47
--- /dev/null
+++ b/compose/facade/core/BaseComposeFacade.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.compose.core
+
+import android.content.Context
+import android.view.View
+
+interface BaseComposeFacade {
+    fun isComposeAvailable(): Boolean
+
+    fun initComposeView(appContext: Context): View
+}
diff --git a/compose/facade/disabled/ComposeFacade.kt b/compose/facade/disabled/ComposeFacade.kt
new file mode 100644
index 0000000..c1cbfff
--- /dev/null
+++ b/compose/facade/disabled/ComposeFacade.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.compose
+
+import android.content.Context
+import android.view.View
+import com.android.launcher3.compose.core.BaseComposeFacade
+
+object ComposeFacade : BaseComposeFacade {
+    override fun isComposeAvailable(): Boolean = false
+
+    override fun initComposeView(appContext: Context): View {
+        error(
+            "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
+                " other function on ComposeFacade."
+        )
+    }
+}
diff --git a/compose/facade/enabled/ComposeFacade.kt b/compose/facade/enabled/ComposeFacade.kt
new file mode 100644
index 0000000..d98a979f
--- /dev/null
+++ b/compose/facade/enabled/ComposeFacade.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.compose
+
+import android.content.Context
+import android.view.View
+import androidx.compose.ui.platform.ComposeView
+import com.android.launcher3.compose.core.BaseComposeFacade
+
+object ComposeFacade : BaseComposeFacade {
+    override fun isComposeAvailable(): Boolean = true
+
+    override fun initComposeView(appContext: Context): View = ComposeView(appContext)
+}
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
index f14cebd..1b9c661 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -52,6 +52,14 @@
         "tests/src/com/android/quickstep/TaplOverviewIconTest.java",
         "tests/src/com/android/quickstep/TaplTestsQuickstep.java",
         "tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
-        "tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java"
+        "tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java",
+    ],
+}
+
+filegroup {
+    name: "launcher3-quickstep-screenshot-tests-src",
+    path: "tests/multivalentScreenshotTests",
+    srcs: [
+        "tests/multivalentScreenshotTests/src/**/*.kt",
     ],
 }
diff --git a/quickstep/compose/facade/core/QuickstepComposeFeatures.kt b/quickstep/compose/facade/core/QuickstepComposeFeatures.kt
new file mode 100644
index 0000000..ca9e5c9
--- /dev/null
+++ b/quickstep/compose/facade/core/QuickstepComposeFeatures.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.compose.core
+
+interface QuickstepComposeFeatures
diff --git a/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt b/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt
new file mode 100644
index 0000000..0a4345a
--- /dev/null
+++ b/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.compose
+
+import android.content.Context
+import com.android.launcher3.compose.ComposeFacade
+import com.android.launcher3.compose.core.BaseComposeFacade
+import com.android.quickstep.compose.core.QuickstepComposeFeatures
+
+object QuickstepComposeFacade : BaseComposeFacade, QuickstepComposeFeatures {
+
+    override fun isComposeAvailable() = ComposeFacade.isComposeAvailable()
+
+    override fun initComposeView(appContext: Context) = ComposeFacade.initComposeView(appContext)
+}
diff --git a/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt b/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt
new file mode 100644
index 0000000..97cd300
--- /dev/null
+++ b/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.compose
+
+import android.content.Context
+import com.android.launcher3.compose.ComposeFacade
+import com.android.launcher3.compose.core.BaseComposeFacade
+import com.android.quickstep.compose.core.QuickstepComposeFeatures
+
+object QuickstepComposeFacade : BaseComposeFacade, QuickstepComposeFeatures {
+    override fun isComposeAvailable() = ComposeFacade.isComposeAvailable()
+
+    override fun initComposeView(appContext: Context) = ComposeFacade.initComposeView(appContext)
+}
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index 797ea45..a11974c 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -41,5 +41,6 @@
         android:textColor="?androidprv:attr/textColorOnAccent"
         android:layout_marginStart="@dimen/split_instructions_start_margin_cancel"
         android:text="@string/toast_split_select_app_cancel"
+        android:textStyle="bold"
         android:visibility="gone"/>
 </com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index ac1a50a..34193d3 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -19,7 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/task"
+    android:id="@+id/task_view_single"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="false"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 64aa7e1..8c7090e 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -19,7 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/task"
+    android:id="@+id/task_view_desktop"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="true"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index da2b29f..cb4b98f 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -24,7 +24,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/task"
+    android:id="@+id/task_view_grouped"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="false"
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index 736706a..e8f3d9d 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -35,6 +35,18 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
+    <com.android.launcher3.taskbar.bubbles.BubbleBarView
+        android:id="@+id/taskbar_bubbles"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/bubblebar_size_with_pointer"
+        android:layout_marginHorizontal="@dimen/transient_taskbar_bottom_margin"
+        android:paddingTop="@dimen/bubblebar_pointer_visible_size"
+        android:visibility="gone"
+        android:gravity="center"
+        android:layout_gravity="bottom"
+        android:clipChildren="false"
+        android:elevation="@dimen/bubblebar_elevation" />
+
     <com.android.launcher3.taskbar.navbutton.NearestTouchFrame
         android:id="@+id/navbuttons_view"
         android:layout_width="match_parent"
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 8bcbb33..037a0f6 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -237,7 +237,7 @@
     <!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
     <string name="toast_split_select_app">Tap another app to use split screen</string>
     <string name="toast_contextual_split_select_app">Choose another app to use split screen</string>
-    <string name="toast_split_select_app_cancel"><b>Cancel</b></string>
+    <string name="toast_split_select_app_cancel">Cancel</string>
     <string name="toast_split_select_cont_desc">Exit split screen selection</string>
     <!-- Label for toast when app selected for split isn't supported. [CHAR_LIMIT=50] -->
     <string name="toast_split_app_unsupported">Choose another app to use split screen</string>
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 6c7fe5b..644705b 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -25,6 +25,7 @@
 import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Launcher;
@@ -38,6 +39,7 @@
 
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.Executor;
 
 /**
  * Controls the visibility of the workspace and the resumed / paused state when desktop mode
@@ -56,7 +58,7 @@
     private boolean mGestureInProgress;
 
     @Nullable
-    private IDesktopTaskListener mDesktopTaskListener;
+    private DesktopTaskListenerImpl mDesktopTaskListener;
 
     public DesktopVisibilityController(Launcher launcher) {
         mLauncher = launcher;
@@ -66,24 +68,7 @@
      * Register a listener with System UI to receive updates about desktop tasks state
      */
     public void registerSystemUiListener() {
-        mDesktopTaskListener = new IDesktopTaskListener.Stub() {
-            @Override
-            public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
-                MAIN_EXECUTOR.execute(() -> {
-                    if (displayId == mLauncher.getDisplayId()) {
-                        if (DEBUG) {
-                            Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
-                        }
-                        setVisibleDesktopTasksCount(visibleTasksCount);
-                    }
-                });
-            }
-
-            @Override
-            public void onStashedChanged(int displayId, boolean stashed) {
-              Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated");
-            }
-        };
+        mDesktopTaskListener = new DesktopTaskListenerImpl(this, mLauncher.getDisplayId());
         SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(mDesktopTaskListener);
     }
 
@@ -92,6 +77,7 @@
      */
     public void unregisterSystemUiListener() {
         SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null);
+        mDesktopTaskListener.release();
         mDesktopTaskListener = null;
     }
 
@@ -355,4 +341,43 @@
          */
         void onDesktopVisibilityChanged(boolean visible);
     }
+
+    /**
+     * Wrapper for the IDesktopTaskListener stub to prevent lingering references to the launcher
+     * activity via the controller.
+     */
+    private static class DesktopTaskListenerImpl extends IDesktopTaskListener.Stub {
+
+        private DesktopVisibilityController mController;
+        private final int mDisplayId;
+
+        DesktopTaskListenerImpl(@NonNull DesktopVisibilityController controller, int displayId) {
+            mController = controller;
+            mDisplayId = displayId;
+        }
+
+        /**
+         * Clears any references to the controller.
+         */
+        void release() {
+            mController = null;
+        }
+
+        @Override
+        public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
+            MAIN_EXECUTOR.execute(() -> {
+                if (mController != null && displayId == mDisplayId) {
+                    if (DEBUG) {
+                        Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
+                    }
+                    mController.setVisibleDesktopTasksCount(visibleTasksCount);
+                }
+            });
+        }
+
+        @Override
+        public void onStashedChanged(int displayId, boolean stashed) {
+            Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated");
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
index c201236..a833ccf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
@@ -16,19 +16,24 @@
 package com.android.launcher3.taskbar;
 
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.SystemUiProxy;
 
 import java.util.ArrayList;
 import java.util.List;
 
 // TODO(b/218912746): Share more behavior to avoid all apps context depending directly on taskbar.
 /** Base for common behavior between taskbar window contexts. */
-public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext {
+public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext,
+        SystemShortcut.BubbleActivityStarter {
 
     protected final LayoutInflater mLayoutInflater;
     private final List<OnDeviceProfileChangeListener> mDPChangeListeners = new ArrayList<>();
@@ -48,6 +53,18 @@
         return mDPChangeListeners;
     }
 
+    @Override
+    public void showShortcutBubble(ShortcutInfo info) {
+        if (info == null) return;
+        SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
+    }
+
+    @Override
+    public void showAppBubble(Intent intent) {
+        if (intent == null || intent.getPackage() == null) return;
+        SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
+    }
+
     /** Callback invoked when a drag is initiated within this context. */
     public abstract void onDragStart();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 3981e43..e5edd51 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -225,9 +225,12 @@
         // Launcher is resumed during the swipe-to-overview gesture under shell-transitions, so
         // avoid updating taskbar state in that situation (when it's non-interactive -- or
         // "background") to avoid premature animations.
-        if (ENABLE_SHELL_TRANSITIONS && isVisible
-                && mLauncher.getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)
-                && !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) {
+        LauncherState state = mLauncher.getStateManager().getState();
+        boolean nonInteractiveState = state.hasFlag(FLAG_NON_INTERACTIVE)
+                && !state.isTaskbarAlignedWithHotseat(mLauncher);
+        if ((ENABLE_SHELL_TRANSITIONS
+                && isVisible
+                && (nonInteractiveState || mSkipLauncherVisibilityChange))) {
             return null;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index ea2adcf..7338485 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -264,11 +264,19 @@
         boolean isThreeButtonNav = mContext.isThreeButtonNav();
         DeviceProfile deviceProfile = mContext.getDeviceProfile();
         Resources resources = mContext.getResources();
-        Point p = !mContext.isUserSetupComplete()
-                ? new Point(0, mControllers.taskbarActivityContext.getSetupWindowSize())
-                : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
-                        mContext.isPhoneMode());
-        mNavButtonsView.getLayoutParams().height = p.y;
+
+        int setupSize = mControllers.taskbarActivityContext.getSetupWindowSize();
+        Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
+                mContext.isPhoneMode());
+        ViewGroup.LayoutParams navButtonsViewLayoutParams = mNavButtonsView.getLayoutParams();
+        navButtonsViewLayoutParams.width = p.x;
+        if (!mContext.isUserSetupComplete()) {
+            // Setup mode in phone mode uses gesture nav.
+            navButtonsViewLayoutParams.height = setupSize;
+        } else {
+            navButtonsViewLayoutParams.height = p.y;
+        }
+        mNavButtonsView.setLayoutParams(navButtonsViewLayoutParams);
 
         mIsImeRenderingNavButtons =
                 InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar();
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 266d0b9..475b516 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -212,7 +212,7 @@
      *                                stashed handle to wrap around the hotseat items.
      */
     public Animator createRevealAnimToIsStashed(boolean isStashed, Rect taskbarToHotseatOffsets) {
-        Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds());
+        Rect visualBounds = mControllers.taskbarViewController.getIconLayoutVisualBounds();
         float startRadius = mStashedHandleRadius;
 
         if (DisplayController.isTransientTaskbar(mActivity)) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 0c730e2..9c954d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -28,6 +28,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.enableTaskbarCustomization;
 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;
@@ -107,11 +108,16 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleCreator;
 import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
 import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.BubblePinController;
-import com.android.launcher3.taskbar.bubbles.BubbleStashController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider;
+import com.android.launcher3.taskbar.bubbles.stashing.DeviceProfileDimensionsProviderAdapter;
+import com.android.launcher3.taskbar.bubbles.stashing.PersistentBubbleStashController;
+import com.android.launcher3.taskbar.bubbles.stashing.TransientBubbleStashController;
 import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator;
 import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator;
 import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
@@ -140,6 +146,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
@@ -149,6 +156,7 @@
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.unfold.updates.RotationChangeProvider;
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
+import com.android.wm.shell.Flags;
 
 import java.io.PrintWriter;
 import java.util.Collections;
@@ -207,9 +215,9 @@
 
     private final LauncherPrefs mLauncherPrefs;
 
-    private final TaskbarFeatureEvaluator mTaskbarFeatureEvaluator;
+    private TaskbarFeatureEvaluator mTaskbarFeatureEvaluator;
 
-    private final TaskbarSpecsEvaluator mTaskbarSpecsEvaluator;
+    private TaskbarSpecsEvaluator mTaskbarSpecsEvaluator;
 
     public TaskbarActivityContext(Context windowContext,
             @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp,
@@ -221,12 +229,14 @@
         applyDeviceProfile(launcherDp);
         final Resources resources = getResources();
 
-        mTaskbarFeatureEvaluator = TaskbarFeatureEvaluator.getInstance(this);
-        mTaskbarSpecsEvaluator = new TaskbarSpecsEvaluator(
-                this,
-                mTaskbarFeatureEvaluator,
-                mDeviceProfile.inv.numRows,
-                mDeviceProfile.inv.numColumns);
+        if (enableTaskbarCustomization()) {
+            mTaskbarFeatureEvaluator = TaskbarFeatureEvaluator.getInstance(this);
+            mTaskbarSpecsEvaluator = new TaskbarSpecsEvaluator(
+                    this,
+                    mTaskbarFeatureEvaluator,
+                    mDeviceProfile.inv.numRows,
+                    mDeviceProfile.inv.numColumns);
+        }
 
         mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, resources, false);
         mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
@@ -247,15 +257,18 @@
         mWindowManager = c.getSystemService(WindowManager.class);
 
         // Inflate views.
-        int taskbarLayout = DisplayController.isTransientTaskbar(this) && !isPhoneMode()
-                ? R.layout.transient_taskbar
-                : R.layout.taskbar;
+        final boolean isTransientTaskbar = DisplayController.isTransientTaskbar(this)
+                && !isPhoneMode();
+        int taskbarLayout = isTransientTaskbar ? R.layout.transient_taskbar : R.layout.taskbar;
         mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false);
         TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
         TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim);
         NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
         StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
-        BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
+        BubbleBarView bubbleBarView = null;
+        if (isTransientTaskbar || Flags.enableBubbleBarInPersistentTaskBar()) {
+            bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
+        }
         StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle);
 
         mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this);
@@ -264,17 +277,28 @@
         Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
         BubbleBarController.onTaskbarRecreated();
         if (BubbleBarController.isBubbleBarEnabled() && bubbleBarView != null) {
+            Optional<BubbleStashedHandleViewController> bubbleHandleController = Optional.empty();
+            if (isTransientTaskbar) {
+                bubbleHandleController = Optional.of(
+                        new BubbleStashedHandleViewController(this, bubbleHandleView));
+            }
+            TaskbarHotseatDimensionsProvider dimensionsProvider =
+                    new DeviceProfileDimensionsProviderAdapter(this);
+            BubbleStashController bubbleStashController = isTransientTaskbar
+                    ? new TransientBubbleStashController(dimensionsProvider, getResources())
+                    : new PersistentBubbleStashController(dimensionsProvider);
             bubbleControllersOptional = Optional.of(new BubbleControllers(
                     new BubbleBarController(this, bubbleBarView),
                     new BubbleBarViewController(this, bubbleBarView),
-                    new BubbleStashController(this),
-                    new BubbleStashedHandleViewController(this, bubbleHandleView),
+                    bubbleStashController,
+                    bubbleHandleController,
                     new BubbleDragController(this),
                     new BubbleDismissController(this, mDragLayer),
                     new BubbleBarPinController(this, mDragLayer,
                             () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
                     new BubblePinController(this, mDragLayer,
-                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize)
+                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+                    new BubbleCreator(this)
             ));
         }
 
@@ -903,7 +927,7 @@
         mControllers.navbarButtonsViewController.updateStateForSysuiFlags(systemUiStateFlags,
                 fromInit);
         boolean isShadeVisible = (systemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0;
-        onNotificationShadeExpandChanged(isShadeVisible, fromInit);
+        onNotificationShadeExpandChanged(isShadeVisible, fromInit || isPhoneMode());
         mControllers.taskbarViewController.setRecentsButtonDisabled(
                 mControllers.navbarButtonsViewController.isRecentsDisabled()
                         || isNavBarKidsModeActive());
@@ -922,8 +946,9 @@
         mControllers.uiController.updateStateForSysuiFlags(systemUiStateFlags);
         mControllers.bubbleControllers.ifPresent(controllers -> {
             controllers.bubbleBarController.updateStateForSysuiFlags(systemUiStateFlags);
-            controllers.bubbleStashedHandleViewController.setIsHomeButtonDisabled(
-                    mControllers.navbarButtonsViewController.isHomeDisabled());
+            controllers.bubbleStashedHandleViewController.ifPresent(controller ->
+                    controller.setIsHomeButtonDisabled(
+                            mControllers.navbarButtonsViewController.isHomeDisabled()));
         });
     }
 
@@ -961,6 +986,7 @@
     public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
         mControllers.navbarButtonsViewController.onTransitionModeUpdated(barMode, checkBarModes);
     }
+
     public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
         mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity()
                 .updateValue(darkIntensity);
@@ -1110,6 +1136,9 @@
      * window.
      */
     public void setTaskbarWindowFocusable(boolean focusable) {
+        if (isPhoneMode()) {
+            return;
+        }
         if (focusable) {
             mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE;
         } else {
@@ -1122,7 +1151,7 @@
      * Applies forcibly show flag to taskbar window iff transient taskbar is unstashed.
      */
     public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) {
-        if (!DisplayController.isTransientTaskbar(this)) {
+        if (!DisplayController.isTransientTaskbar(this) || isPhoneMode()) {
             return;
         }
         if (shouldForceShow) {
@@ -1389,7 +1418,8 @@
                     if (foundTask != null) {
                         TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
                         if (foundTaskView != null
-                                && foundTaskView.isVisibleToUser()) {
+                                && foundTaskView.isVisibleToUser()
+                                && !(foundTaskView instanceof DesktopTaskView)) {
                             TestLogging.recordEvent(
                                     TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
                             foundTaskView.launchTasks();
@@ -1440,7 +1470,6 @@
                         return;
                     }
                 }
-
                 startActivity(intent);
             } else {
                 getSystemService(LauncherApps.class).startMainActivity(
@@ -1665,7 +1694,7 @@
      * @param exclude {@code true} then the magnification region computation will omit the window.
      */
     public void excludeFromMagnificationRegion(boolean exclude) {
-        if (mIsExcludeFromMagnificationRegion == exclude) {
+        if (mIsExcludeFromMagnificationRegion == exclude || isPhoneMode()) {
             return;
         }
 
@@ -1707,10 +1736,12 @@
         return mControllers.taskbarStashController.isInStashedLauncherState();
     }
 
+    @Nullable
     public TaskbarFeatureEvaluator getTaskbarFeatureEvaluator() {
         return mTaskbarFeatureEvaluator;
     }
 
+    @Nullable
     public TaskbarSpecsEvaluator getTaskbarSpecsEvaluator() {
         return mTaskbarSpecsEvaluator;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 0645972..d94d9175 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -165,6 +165,7 @@
         taskbarOverlayController.init(this);
         taskbarAllAppsController.init(this, sharedState.allAppsVisible);
         navButtonController.init(this);
+        bubbleControllers.ifPresent(controllers -> controllers.init(this));
         taskbarInsetsController.init(this);
         voiceInteractionWindowController.init(this);
         taskbarRecentAppsController.init(this);
@@ -172,7 +173,6 @@
         taskbarEduTooltipController.init(this);
         keyboardQuickSwitchController.init(this);
         taskbarPinningController.init(this, mSharedState);
-        bubbleControllers.ifPresent(controllers -> controllers.init(this));
 
         mControllersToLog = new LoggableTaskbarController[] {
                 taskbarDragController, navButtonController, navbarButtonsViewController,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
index 6ac862e..8a86402 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
@@ -85,6 +85,9 @@
 
     /** Update values tracked via sysui flags. */
     public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) {
+        if (mContext.isPhoneMode()) {
+            return;
+        }
         mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0;
         if (mContext.isNavBarForceVisible()) {
             if (mIsImmersiveMode) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 2103ebb..9dee473 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -64,6 +64,10 @@
     companion object {
         private const val INDEX_LEFT = 0
         private const val INDEX_RIGHT = 1
+
+        private fun Region.addBoundsToRegion(bounds: Rect?) {
+            bounds?.let { op(it, Region.Op.UNION) }
+        }
     }
 
     /** The bottom insets taskbar provides to the IME when IME is visible. */
@@ -128,46 +132,25 @@
             }
         }
 
+        val bubbleControllers = controllers.bubbleControllers.getOrNull()
         val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight
         val bubblesTouchableHeight =
-            if (controllers.bubbleControllers.isPresent) {
-                controllers.bubbleControllers.get().bubbleStashController.touchableHeight
-            } else {
-                0
-            }
+            bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0
+        // add bounds for task bar and bubble bar stash controllers
         val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight)
-
-        if (
-            controllers.bubbleControllers.isPresent &&
-                controllers.bubbleControllers.get().bubbleStashController.isBubblesShowingOnHome
-        ) {
-            val iconBounds =
-                controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
-            defaultTouchableRegion.set(
-                iconBounds.left,
-                iconBounds.top,
-                iconBounds.right,
-                iconBounds.bottom
-            )
-        } else {
-            defaultTouchableRegion.set(
-                0,
-                windowLayoutParams.height - touchableHeight,
-                context.deviceProfile.widthPx,
-                windowLayoutParams.height
-            )
-
-            // if there's an animating bubble add it to the touch region so that it's clickable
-            val isAnimatingNewBubble =
-                controllers.bubbleControllers
-                    .getOrNull()
-                    ?.bubbleBarViewController
-                    ?.isAnimatingNewBubble
-                    ?: false
-            if (isAnimatingNewBubble) {
-                val iconBounds =
-                    controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
-                defaultTouchableRegion.op(iconBounds, Region.Op.UNION)
+        defaultTouchableRegion.set(
+            0,
+            windowLayoutParams.height - touchableHeight,
+            context.deviceProfile.widthPx,
+            windowLayoutParams.height
+        )
+        if (bubbleControllers != null) {
+            val bubbleBarViewController = bubbleControllers.bubbleBarViewController
+            val isBubbleBarVisible = bubbleControllers.bubbleStashController.isBubbleBarVisible()
+            val isAnimatingNewBubble = bubbleBarViewController.isAnimatingNewBubble
+            // if bubble bar is visible or animating new bubble, add bar bounds to the touch region
+            if (isBubbleBarVisible || isAnimatingNewBubble) {
+                defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
             }
         }
 
@@ -358,13 +341,6 @@
      */
     fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) {
         insetsInfo.touchableRegion.setEmpty()
-        // Always have nav buttons be touchable
-        controllers.navbarButtonsViewController.addVisibleButtonsRegion(
-            context.dragLayer,
-            insetsInfo.touchableRegion
-        )
-        debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
-
         val bubbleBarVisible =
             controllers.bubbleControllers.isPresent &&
                 controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
@@ -426,7 +402,7 @@
 
                 // Include the bounds of the bubble bar in the touchable region if they exist.
                 if (bubbleBarBounds != null) {
-                    region.op(bubbleBarBounds, Region.Op.UNION)
+                    region.addBoundsToRegion(bubbleBarBounds)
                 }
                 insetsInfo.touchableRegion.set(region)
                 debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview"
@@ -443,6 +419,12 @@
             debugTouchableRegion.lastSetTouchableReason =
                 "Icons are not visible, but other components such as 3 buttons might be"
         }
+        // Always have nav buttons be touchable
+        controllers.navbarButtonsViewController.addVisibleButtonsRegion(
+            context.dragLayer,
+            insetsInfo.touchableRegion
+        )
+        debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
         context.excludeFromMagnificationRegion(insetsIsTouchableRegion)
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 20ab32e..e6b3acd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -470,7 +470,8 @@
                 // We're changing state to home, should close open popups e.g. Taskbar AllApps
                 handleOpenFloatingViews = true;
             }
-            if (mLauncherState == LauncherState.OVERVIEW) {
+            if (mLauncherState == LauncherState.OVERVIEW
+                    && !mControllers.taskbarActivityContext.isPhoneMode()) {
                 // Calling to update the insets in TaskbarInsetController#updateInsetsTouchability
                 mControllers.taskbarActivityContext.notifyUpdateLayoutParams();
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index b697590..e80ad7a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -53,6 +53,7 @@
 import com.android.quickstep.util.LogUtils;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
@@ -69,6 +70,9 @@
     private static final SystemShortcut.Factory<BaseTaskbarContext>
             APP_INFO = SystemShortcut.AppInfo::new;
 
+    private static final SystemShortcut.Factory<BaseTaskbarContext>
+            BUBBLE = SystemShortcut.BubbleShortcut::new;
+
     private final TaskbarActivityContext mContext;
     private final PopupDataProvider mPopupDataProvider;
 
@@ -182,10 +186,13 @@
     // Create a Stream of all applicable system shortcuts
     private Stream<SystemShortcut.Factory> getSystemShortcuts() {
         // append split options to APP_INFO shortcut, the order here will reflect in the popup
-        return Stream.concat(
-                Stream.of(APP_INFO),
-                mControllers.uiController.getSplitMenuOptions()
-        );
+        ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>();
+        shortcuts.add(APP_INFO);
+        shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList());
+        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+            shortcuts.add(BUBBLE);
+        }
+        return shortcuts.stream();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 49fc0dd..1a168a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -27,8 +27,8 @@
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
-import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
-import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import java.io.PrintWriter
 
 /**
@@ -44,9 +44,9 @@
     private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?,
 ) : LoggableTaskbarController {
 
-    // TODO(b/335401172): unify DesktopMode checks in Launcher.
     var canShowRunningApps =
-        DESKTOP_WINDOWING_MODE.isEnabled(context) && enableDesktopWindowingTaskbarRunningApps()
+        DesktopModeStatus.canEnterDesktopMode(context) &&
+            DesktopModeFlags.TASKBAR_RUNNING_APPS.isEnabled(context)
         @VisibleForTesting
         set(isEnabledFromTest) {
             field = isEnabledFromTest
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 48d2bc2..4df0223 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -20,14 +20,15 @@
 import static com.android.launcher3.taskbar.bubbles.BubbleBarController.isBubbleBarEnabled;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
-import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
+import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
 
 import android.animation.ObjectAnimator;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -65,6 +66,7 @@
      */
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
+        onTaskbarVisibilityChanged(mControllers.taskbarViewController.getTaskbarVisibility());
     }
 
     /**
@@ -85,6 +87,10 @@
      * Updates the scrim state based on the flags.
      */
     public void updateStateForSysuiFlags(@SystemUiStateFlags long stateFlags, boolean skipAnim) {
+        if (mActivity.isPhoneMode()) {
+            // There is no scrim for the bar in the phone mode.
+            return;
+        }
         if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) {
             // These scrims aren't used if bubble bar & transient taskbar are active.
             return;
@@ -96,10 +102,20 @@
     private boolean shouldShowScrim() {
         final boolean bubblesExpanded = (mSysUiStateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0;
         boolean isShadeVisible = (mSysUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0;
+        BubbleControllers bubbleControllers = mActivity.getBubbleControllers();
+        boolean isBubbleControllersPresented = bubbleControllers != null;
+        // when the taskbar is in persistent mode, we hide the task bar icons on bubble bar expand,
+        // which makes the taskbar invisible, so need to check if the bubble bar is not on home
+        // to show the scrim view
+        boolean showScrimForBubbles = bubblesExpanded
+                && !mTaskbarVisible
+                && isBubbleControllersPresented
+                && !DisplayController.isTransientTaskbar(mActivity)
+                && !bubbleControllers.bubbleStashController.isBubblesShowingOnHome();
         return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible()
                 && !isShadeVisible
                 && !mControllers.taskbarStashController.isStashed()
-                && mTaskbarVisible;
+                && (mTaskbarVisible || showScrimForBubbles);
     }
 
     private float getScrimAlpha() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 5d6fdc1..e33e293 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -1022,6 +1022,10 @@
 
     /** 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;
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
index 144c0c2..5a5d6d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -95,7 +95,8 @@
         mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
         mControllers.bubbleControllers.ifPresent(controllers -> {
             controllers.bubbleBarViewController.setTranslationYForSwipe(transY);
-            controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY);
+            controllers.bubbleStashedHandleViewController.ifPresent(
+                    controller -> controller.setTranslationYForSwipe(transY));
         });
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index a2278ec..42bf8db 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -61,6 +61,8 @@
     // Initialized in init.
     protected TaskbarControllers mControllers;
 
+    protected boolean mSkipLauncherVisibilityChange;
+
     @CallSuper
     protected void init(TaskbarControllers taskbarControllers) {
         mControllers = taskbarControllers;
@@ -174,11 +176,11 @@
                 || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
     }
 
-    /** Checks if the given {@link MotionEvent} is over the bubble bar stash handle. */
-    public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) {
+    /** Checks if the given {@link MotionEvent} is over the bubble bar views. */
+    public boolean isEventOverBubbleBarViews(MotionEvent ev) {
         return mControllers.bubbleControllers.map(
                 bubbleControllers ->
-                        bubbleControllers.bubbleStashController.isEventOverStashHandle(ev))
+                        bubbleControllers.bubbleStashController.isEventOverBubbleBarViews(ev))
                 .orElse(false);
     }
 
@@ -418,4 +420,12 @@
     public void setUserIsNotGoingHome(boolean isNotGoingHome) {
         mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome);
     }
+
+    /**
+     * Sets whether to prevent taskbar from reacting to launcher visibility during the recents
+     * transition animation.
+     */
+    public void setSkipLauncherVisibilityChange(boolean skip) {
+        mSkipLauncherVisibilityChange = skip;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index e58069a..fc76972 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -669,8 +669,20 @@
         return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
     }
 
+    /**
+     * Gets visual bounds of the taskbar view. The visual bounds correspond to the taskbar touch
+     * area, rather than layout placement in the parent view.
+     */
+    public Rect getIconLayoutVisualBounds() {
+        return new Rect(mIconLayoutBounds);
+    }
+
+    /** Gets taskbar layout bounds in parent view. */
     public Rect getIconLayoutBounds() {
-        return mIconLayoutBounds;
+        Rect actualBounds = new Rect(mIconLayoutBounds);
+        actualBounds.top = getTop();
+        actualBounds.bottom = getBottom();
+        return actualBounds;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 293e87c..b8b85d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -96,7 +96,9 @@
     public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4;
     public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 5;
     public static final int ALPHA_INDEX_SMALL_SCREEN = 6;
-    private static final int NUM_ALPHA_CHANNELS = 7;
+
+    public static final int ALPHA_INDEX_BUBBLE_BAR = 7;
+    private static final int NUM_ALPHA_CHANNELS = 8;
 
     private static boolean sEnableModelLoadingForTests = true;
 
@@ -233,6 +235,13 @@
         mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
     }
 
+    /**
+     * Gets the taskbar {@link View.Visibility visibility}.
+     */
+    public int getTaskbarVisibility() {
+        return mTaskbarView.getVisibility();
+    }
+
     public boolean areIconsVisible() {
         return mTaskbarView.areIconsVisible();
     }
@@ -265,6 +274,10 @@
         OneShotPreDrawListener.add(mTaskbarView, listener);
     }
 
+    public Rect getIconLayoutVisualBounds() {
+        return mTaskbarView.getIconLayoutVisualBounds();
+    }
+
     public Rect getIconLayoutBounds() {
         return mTaskbarView.getIconLayoutBounds();
     }
@@ -455,14 +468,14 @@
         if (mControllers.getSharedState().startTaskbarVariantIsTransient) {
             float transY =
                     mTransientTaskbarDp.taskbarBottomMargin + (mTransientTaskbarDp.taskbarHeight
-                            - mTaskbarView.getIconLayoutBounds().bottom)
+                            - mTaskbarView.getIconLayoutVisualBounds().bottom)
                             - (mPersistentTaskbarDp.taskbarHeight
                                     - mTransientTaskbarDp.taskbarIconSize) / 2f;
             taskbarIconTranslationYForPinningValue = mapRange(scale, 0f, transY);
         } else {
             float transY =
                     -mTransientTaskbarDp.taskbarBottomMargin + (mPersistentTaskbarDp.taskbarHeight
-                            - mTaskbarView.getIconLayoutBounds().bottom)
+                            - mTaskbarView.getIconLayoutVisualBounds().bottom)
                             - (mTransientTaskbarDp.taskbarHeight
                                     - mTransientTaskbarDp.taskbarIconSize) / 2f;
             taskbarIconTranslationYForPinningValue = mapRange(scale, transY, 0f);
diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
index 619c9c4..c380c8d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
@@ -110,7 +110,7 @@
     }
 
     fun setIsVoiceInteractionWindowVisible(visible: Boolean, skipAnim: Boolean) {
-        if (isVoiceInteractionWindowVisible == visible) {
+        if (isVoiceInteractionWindowVisible == visible || context.isPhoneMode) {
             return
         }
         isVoiceInteractionWindowVisible = visible
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 5be0171..33d8a84 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -15,13 +15,8 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 
-import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -34,35 +29,13 @@
 import android.annotation.BinderThread;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.content.pm.ShortcutInfo;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Matrix;
-import android.graphics.Path;
 import android.graphics.Point;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
 import android.os.Bundle;
 import android.os.SystemProperties;
-import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.util.PathParser;
-import android.view.LayoutInflater;
 
-import androidx.appcompat.content.res.AppCompatResources;
-
-import com.android.internal.graphics.ColorUtils;
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.BubbleIconFactory;
-import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -76,6 +49,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
@@ -134,18 +108,16 @@
 
     private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor(
             new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND));
-    private final LauncherApps mLauncherApps;
-    private final BubbleIconFactory mIconFactory;
     private final SystemUiProxy mSystemUiProxy;
 
     private BubbleBarItem mSelectedBubble;
-    private BubbleBarOverflow mOverflowBubble;
 
     private ImeVisibilityChecker mImeVisibilityChecker;
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleStashController mBubbleStashController;
-    private BubbleStashedHandleViewController mBubbleStashedHandleViewController;
+    private Optional<BubbleStashedHandleViewController> mBubbleStashedHandleViewController;
     private BubblePinController mBubblePinController;
+    private BubbleCreator mBubbleCreator;
 
     // Cache last sent top coordinate to avoid sending duplicate updates to shell
     private int mLastSentBubbleBarTop;
@@ -166,6 +138,8 @@
         List<RemovedBubble> removedBubbles;
         List<String> bubbleKeysInOrder;
         Point expandedViewDropTargetSize;
+        boolean showOverflow;
+        boolean showOverflowChanged;
 
         // These need to be loaded in the background
         BubbleBarBubble addedBubble;
@@ -184,6 +158,8 @@
             removedBubbles = update.removedBubbles;
             bubbleKeysInOrder = update.bubbleKeysInOrder;
             expandedViewDropTargetSize = update.expandedViewDropTargetSize;
+            showOverflow = update.showOverflow;
+            showOverflowChanged = update.showOverflowChanged;
         }
     }
 
@@ -196,13 +172,6 @@
         if (sBubbleBarEnabled) {
             mSystemUiProxy.setBubblesListener(this);
         }
-        mLauncherApps = context.getSystemService(LauncherApps.class);
-        mIconFactory = new BubbleIconFactory(context,
-                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
-                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
-                context.getResources().getColor(R.color.important_conversation),
-                context.getResources().getDimensionPixelSize(
-                        com.android.internal.R.dimen.importance_ring_stroke_width));
     }
 
     public void onDestroy() {
@@ -217,12 +186,14 @@
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
         mBubblePinController = bubbleControllers.bubblePinController;
+        mBubbleCreator = bubbleControllers.bubbleCreator;
 
         bubbleControllers.runAfterInit(() -> {
             mBubbleBarViewController.setHiddenForBubbles(
                     !sBubbleBarEnabled || mBubbles.isEmpty());
-            mBubbleStashedHandleViewController.setHiddenForBubbles(
-                    !sBubbleBarEnabled || mBubbles.isEmpty());
+            mBubbleStashedHandleViewController.ifPresent(
+                    controller -> controller.setHiddenForBubbles(
+                            !sBubbleBarEnabled || mBubbles.isEmpty()));
             mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
                     key -> setSelectedBubbleInternal(mBubbles.get(key)));
             mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged);
@@ -230,27 +201,6 @@
     }
 
     /**
-     * Creates and adds the overflow bubble to the bubble bar if it hasn't been created yet.
-     *
-     * <p>This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid inflating
-     * the overflow multiple times.
-     */
-    private void createAndAddOverflowIfNeeded() {
-        if (mOverflowBubble == null) {
-            BubbleBarOverflow overflow = createOverflow(mContext);
-            MAIN_EXECUTOR.execute(() -> {
-                // we're on the main executor now, so check that the overflow hasn't been created
-                // again to avoid races.
-                if (mOverflowBubble == null) {
-                    mBubbleBarViewController.addBubble(
-                            overflow, /* isExpanding= */ false, /* suppressAnimation= */ true);
-                    mOverflowBubble = overflow;
-                }
-            });
-        }
-    }
-
-    /**
      * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags.
      */
     public void updateStateForSysuiFlags(@SystemUiStateFlags long flags) {
@@ -258,10 +208,11 @@
         mBubbleBarViewController.setHiddenForSysui(hideBubbleBar);
 
         boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0;
-        mBubbleStashedHandleViewController.setHiddenForSysui(hideHandleView);
+        mBubbleStashedHandleViewController.ifPresent(
+                controller -> controller.setHiddenForSysui(hideHandleView));
 
         boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0;
-        mBubbleStashController.onSysuiLockedStateChange(sysuiLocked);
+        mBubbleStashController.setSysuiLocked(sysuiLocked);
     }
 
     //
@@ -279,23 +230,25 @@
                 || !update.currentBubbleList.isEmpty()) {
             // We have bubbles to load
             BUBBLE_STATE_EXECUTOR.execute(() -> {
-                createAndAddOverflowIfNeeded();
                 if (update.addedBubble != null) {
-                    viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
+                    viewUpdate.addedBubble = mBubbleCreator.populateBubble(mContext,
+                            update.addedBubble,
+                            mBarView,
                             null /* existingBubble */);
                 }
                 if (update.updatedBubble != null) {
                     BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
                     viewUpdate.updatedBubble =
-                            populateBubble(mContext, update.updatedBubble, mBarView,
+                            mBubbleCreator.populateBubble(mContext, update.updatedBubble,
+                                    mBarView,
                                     existingBubble);
                 }
                 if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
                     List<BubbleBarBubble> currentBubbles = new ArrayList<>();
                     for (int i = 0; i < update.currentBubbleList.size(); i++) {
-                        BubbleBarBubble b =
-                                populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
-                                        null /* existingBubble */);
+                        BubbleBarBubble b = mBubbleCreator.populateBubble(mContext,
+                                update.currentBubbleList.get(i), mBarView,
+                                null /* existingBubble */);
                         currentBubbles.add(b);
                     }
                     viewUpdate.currentBubbles = currentBubbles;
@@ -322,7 +275,13 @@
 
         BubbleBarBubble bubbleToSelect = null;
 
-        if (update.addedBubble != null && update.removedBubbles.size() == 1) {
+        if (Flags.enableOptionalBubbleOverflow()
+                && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null
+                && update.removedBubbles.isEmpty()) {
+            // A bubble was added from the overflow (& now it's empty / not showing)
+            mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
+        } else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
             // we're adding and removing a bubble at the same time. handle this as a single update.
             RemovedBubble removedBubble = update.removedBubbles.get(0);
             BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey());
@@ -336,11 +295,17 @@
                 Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
             }
         } else {
+            boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow()
+                    && update.showOverflowChanged && update.showOverflow;
             if (!update.removedBubbles.isEmpty()) {
                 for (int i = 0; i < update.removedBubbles.size(); i++) {
                     RemovedBubble removedBubble = update.removedBubbles.get(i);
                     BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
-                    if (bubble != null) {
+                    if (bubble != null && overflowNeedsToBeAdded) {
+                        // First removal, show the overflow
+                        overflowNeedsToBeAdded = false;
+                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
+                    } else if (bubble != null) {
                         mBubbleBarViewController.removeBubble(bubble);
                     } else {
                         Log.w(TAG, "trying to remove bubble that doesn't exist: "
@@ -353,6 +318,11 @@
                 mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
                         suppressAnimation);
             }
+            if (Flags.enableOptionalBubbleOverflow()
+                    && update.showOverflowChanged
+                    && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) {
+                mBubbleBarViewController.showOverflow(update.showOverflow);
+            }
         }
 
         // if a bubble was updated upstream, but removed before the update was received, add it back
@@ -384,10 +354,14 @@
                 }
             }
         }
+        if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) {
+            mBubbleBarViewController.showOverflow(true);
+        }
 
         // Adds and removals have happened, update visibility before any other visual changes.
         mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
-        mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
+        mBubbleStashedHandleViewController.ifPresent(
+                controller -> controller.setHiddenForBubbles(mBubbles.isEmpty()));
 
         if (mBubbles.isEmpty()) {
             // all bubbles were removed. clear the selected bubble
@@ -525,133 +499,6 @@
     // Loading data for the bubbles
     //
 
-    @Nullable
-    private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
-            @Nullable BubbleBarBubble existingBubble) {
-        String appName;
-        Bitmap badgeBitmap;
-        Bitmap bubbleBitmap;
-        Path dotPath;
-        int dotColor;
-
-        boolean isImportantConvo = b.isImportantConversation();
-
-        ShortcutRequest.QueryResult result = new ShortcutRequest(context,
-                new UserHandle(b.getUserId()))
-                .forPackage(b.getPackageName(), b.getShortcutId())
-                .query(FLAG_MATCH_DYNAMIC
-                        | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
-                        | FLAG_MATCH_CACHED
-                        | FLAG_GET_PERSONS_DATA);
-
-        ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
-        if (shortcutInfo == null) {
-            Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey()
-                    + " with shortcutId: " + b.getShortcutId());
-        }
-
-        ApplicationInfo appInfo;
-        try {
-            appInfo = mLauncherApps.getApplicationInfo(
-                    b.getPackageName(),
-                    0,
-                    new UserHandle(b.getUserId()));
-        } catch (PackageManager.NameNotFoundException e) {
-            // If we can't find package... don't think we should show the bubble.
-            Log.w(TAG, "Unable to find packageName: " + b.getPackageName());
-            return null;
-        }
-        if (appInfo == null) {
-            Log.w(TAG, "Unable to find appInfo: " + b.getPackageName());
-            return null;
-        }
-        PackageManager pm = context.getPackageManager();
-        appName = String.valueOf(appInfo.loadLabel(pm));
-        Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
-        Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId()));
-
-        // Badged bubble image
-        Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
-                b.getIcon());
-        if (bubbleDrawable == null) {
-            // Default to app icon
-            bubbleDrawable = appIcon;
-        }
-
-        BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
-        badgeBitmap = badgeBitmapInfo.icon;
-
-        float[] bubbleBitmapScale = new float[1];
-        bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
-
-        // Dot color & placement
-        Path iconPath = PathParser.createPathFromPathData(
-                context.getResources().getString(
-                        com.android.internal.R.string.config_icon_mask));
-        Matrix matrix = new Matrix();
-        float scale = bubbleBitmapScale[0];
-        float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
-        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
-                radius /* pivot y */);
-        iconPath.transform(matrix);
-        dotPath = iconPath;
-        dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
-                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
-
-        if (existingBubble == null) {
-            LayoutInflater inflater = LayoutInflater.from(context);
-            BubbleView bubbleView = (BubbleView) inflater.inflate(
-                    R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
-
-            BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
-                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
-            bubbleView.setBubble(bubble);
-            return bubble;
-        } else {
-            // If we already have a bubble (so it already has an inflated view), update it.
-            existingBubble.setInfo(b);
-            existingBubble.setBadge(badgeBitmap);
-            existingBubble.setIcon(bubbleBitmap);
-            existingBubble.setDotColor(dotColor);
-            existingBubble.setDotPath(dotPath);
-            existingBubble.setAppName(appName);
-            return existingBubble;
-        }
-    }
-
-    private BubbleBarOverflow createOverflow(Context context) {
-        Bitmap bitmap = createOverflowBitmap(context);
-        LayoutInflater inflater = LayoutInflater.from(context);
-        BubbleView bubbleView = (BubbleView) inflater.inflate(
-                R.layout.bubble_bar_overflow_button, mBarView, false /* attachToRoot */);
-        BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
-        bubbleView.setOverflow(overflow, bitmap);
-        return overflow;
-    }
-
-    private Bitmap createOverflowBitmap(Context context) {
-        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
-                R.drawable.bubble_ic_overflow_button);
-
-        final TypedArray ta = mContext.obtainStyledAttributes(
-                new int[]{
-                        R.attr.materialColorOnPrimaryFixed,
-                        R.attr.materialColorPrimaryFixed
-                });
-        int overflowIconColor = ta.getColor(0, Color.WHITE);
-        int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
-        ta.recycle();
-
-        iconDrawable.setTint(overflowIconColor);
-
-        int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
-        Drawable foreground = new InsetDrawable(iconDrawable, inset);
-        Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
-                foreground);
-
-        return mIconFactory.createBadgedIconBitmap(drawable).icon;
-    }
-
     private void onBubbleBarBoundsChanged() {
         int newTop = mBarView.getRestingTopPositionOnScreen();
         if (newTop != mLastSentBubbleBarTop) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
index 9e5ffc9..a6b0860 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.stashing.BubbleStashController
 import com.android.wm.shell.common.bubbles.BaseBubblePinController
 import com.android.wm.shell.common.bubbles.BubbleBarLocation
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 7d27a90..a2746df 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -418,6 +418,7 @@
         LayoutParams lp = (LayoutParams) getLayoutParams();
         lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
         setLayoutParams(lp); // triggers a relayout
+        updateBubbleAccessibilityStates();
     }
 
     /**
@@ -660,13 +661,25 @@
         return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY();
     }
 
-    /**
-     * Updates the bounds with translation that may have been applied and returns the result.
-     */
+    /** Returns the bounds with translation that may have been applied. */
     public Rect getBubbleBarBounds() {
-        mBubbleBarBounds.top = getTop() + (int) getTranslationY() + mPointerSize;
-        mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY();
-        return mBubbleBarBounds;
+        Rect bounds = new Rect(mBubbleBarBounds);
+        bounds.top = getTop() + (int) getTranslationY() + mPointerSize;
+        bounds.bottom = getBottom() + (int) getTranslationY();
+        return bounds;
+    }
+
+    /** Returns the expanded bounds with translation that may have been applied. */
+    public Rect getBubbleBarExpandedBounds() {
+        Rect expandedBounds = getBubbleBarBounds();
+        if (!isExpanded() || isExpanding()) {
+            if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+                expandedBounds.right = expandedBounds.left + (int) expandedWidth();
+            } else {
+                expandedBounds.left = expandedBounds.right - (int) expandedWidth();
+            }
+        }
+        return expandedBounds;
     }
 
     /**
@@ -715,11 +728,13 @@
     public void addBubble(BubbleView bubble) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
+        final int index = bubble.isOverflow() ? getChildCount() : 0;
+
         if (isExpanded()) {
             // if we're expanded scale the new bubble in
             bubble.setScaleX(0f);
             bubble.setScaleY(0f);
-            addView(bubble, 0, lp);
+            addView(bubble, index, lp);
             bubble.showDotIfNeeded(/* animate= */ false);
 
             mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
@@ -748,23 +763,33 @@
             };
             mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
         } else {
-            addView(bubble, 0, lp);
+            addView(bubble, index, lp);
         }
     }
 
     /** Add a new bubble and remove an old bubble from the bubble bar. */
-    public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) {
+    public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
+        boolean isOverflowSelected = mSelectedBubbleView.isOverflow();
+        boolean removingOverflow = removedBubble.isOverflow();
+        boolean addingOverflow = addedBubble.isOverflow();
+
         if (!isExpanded()) {
             removeView(removedBubble);
-            addView(addedBubble, 0, lp);
+            int index = addingOverflow ? getChildCount() : 0;
+            addView(addedBubble, index, lp);
             return;
         }
+        int index = addingOverflow ? getChildCount() : 0;
         addedBubble.setScaleX(0f);
         addedBubble.setScaleY(0f);
-        addView(addedBubble, 0, lp);
+        addView(addedBubble, index, lp);
 
+        if (isOverflowSelected && removingOverflow) {
+            // The added bubble will be selected
+            mSelectedBubbleView = addedBubble;
+        }
         int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
         int indexOfBubbleToRemove = indexOfChild(removedBubble);
 
@@ -875,6 +900,33 @@
         updateNotificationDotsIfCollapsed();
     }
 
+    /**
+     * Return child views in the order which they are shown on the screen.
+     * <p>
+     * Child views (bubbles) are always ordered based on recency. The most recent bubble is at index
+     * 0.
+     * For example if the child views are (1)(2)(3) then (1) is the most recent bubble and at index
+     * 0.<br>
+     *
+     * How bubbles show up on the screen depends on the bubble bar location. If the bar is on the
+     * left, the most recent bubble is shown on the right. The bubbles from the example above would
+     * be shown as: (3)(2)(1).<br>
+     *
+     * If bubble bar is on the right, then the most recent bubble is on the left. Bubbles from the
+     * example above would be shown as: (1)(2)(3).
+     */
+    private List<View> getChildViewsInOnScreenOrder() {
+        List<View> childViews = new ArrayList<>(getChildCount());
+        for (int i = 0; i < getChildCount(); i++) {
+            childViews.add(getChildAt(i));
+        }
+        if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+            // Visually child views are shown in reverse order when bar is on the left
+            return childViews.reversed();
+        }
+        return childViews;
+    }
+
     private void updateNotificationDotsIfCollapsed() {
         if (isExpanded()) {
             return;
@@ -924,7 +976,7 @@
         final float currentWidth = getWidth();
         final float expandedWidth = expandedWidth();
         final float collapsedWidth = collapsedWidth();
-        int bubbleCount = getChildCount();
+        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
@@ -932,7 +984,7 @@
         final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
         // elevation state is opposite to widthState - when expanded all icons are flat
         float elevationState = (1 - widthState);
-        for (int i = 0; i < bubbleCount; i++) {
+        for (int i = 0; i < childCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
                 // Skip the dragged bubble. Its translation is managed by the drag controller.
@@ -951,9 +1003,9 @@
             bv.setTranslationY(ty);
 
             // the position of the bubble when the bar is fully expanded
-            final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
+            final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft);
             // the position of the bubble when the bar is fully collapsed
-            final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft);
+            final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft);
 
             // slowly animate elevation while keeping correct Z ordering
             float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
@@ -981,13 +1033,10 @@
                 final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
                 final float targetX = collapsedX + collapsedBarShift;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
-                // If we're fully collapsed, hide all bubbles except for the first 2. If there are
-                // only 2 bubbles, hide the second bubble as well because it's the overflow.
+                // If we're fully collapsed, hide all bubbles except for the first 2, excluding
+                // the overflow.
                 if (widthState == 0) {
-                    if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
-                        bv.setAlpha(0);
-                    } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1
-                            && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) {
+                    if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
                         bv.setAlpha(0);
                     } else {
                         bv.setAlpha(1);
@@ -1043,22 +1092,26 @@
         return translationX - getScaleIconShift();
     }
 
-    private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) {
-        if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
+    private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) {
+        if (bubbleIndex < 0 || bubbleIndex >= childCount) {
             return 0;
         }
         float translationX;
         if (onLeft) {
-            // Shift the first bubble only if there are more bubbles in addition to overflow
-            translationX = mBubbleBarPadding + (
-                    bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
-                            ? mIconOverlapAmount : 0);
+            // Shift the first bubble only if there are more bubbles
+            if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
+                translationX = mIconOverlapAmount;
+            } else {
+                translationX = 0f;
+            }
         } else {
-            translationX = mBubbleBarPadding + (
-                    bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED
-                            ? 0 : mIconOverlapAmount);
+            if (bubbleIndex == 1 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
+                translationX = mIconOverlapAmount;
+            } else {
+                translationX = 0f;
+            }
         }
-        return translationX - getScaleIconShift();
+        return mBubbleBarPadding + translationX - getScaleIconShift();
     }
 
     /**
@@ -1239,6 +1292,13 @@
     }
 
     /**
+     * Returns whether the bubble bar is expanding.
+     */
+    public boolean isExpanding() {
+        return mWidthAnimator.isRunning() && mIsBarExpanded;
+    }
+
+    /**
      * Get width of the bubble bar as if it would be expanded.
      *
      * @return width of the bubble bar in its expanded state, regardless of current width
@@ -1256,15 +1316,20 @@
     }
 
     private float collapsedWidth() {
-        final int childCount = getChildCount();
+        final int bubbleChildCount = getBubbleChildCount();
         final float horizontalPadding = 2 * mBubbleBarPadding;
-        // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
-        // Otherwise just the first bubble should be visible because we don't show the overflow.
-        return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED
+        // If there are more than 2 bubbles, the first 2 should be visible when collapsed,
+        // excluding the overflow.
+        return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED
                 ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding
                 : getScaledIconSize() + horizontalPadding;
     }
 
+    /** Returns the child count excluding the overflow if it's present. */
+    private int getBubbleChildCount() {
+        return hasOverflow() ? getChildCount() - 1 : getChildCount();
+    }
+
     private float getBubbleBarExpandedHeight() {
         return getBubbleBarCollapsedHeight() + mPointerSize;
     }
@@ -1303,8 +1368,8 @@
         return mIsAnimatingNewBubble;
     }
 
-    private boolean hasOverview() {
-        // Overview is always the last bubble
+    private boolean hasOverflow() {
+        // Overflow is always the last bubble
         View lastChild = getChildAt(getChildCount() - 1);
         if (lastChild instanceof BubbleView bubbleView) {
             return bubbleView.getBubble() instanceof BubbleBarOverflow;
@@ -1313,21 +1378,39 @@
     }
 
     private void updateBubbleAccessibilityStates() {
-        final int childA11y;
         if (mIsBarExpanded) {
             // Bar is expanded, focus on the bubbles
             setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-            childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+
+            // Set up a11y navigation order. Get list of child views in the order they are shown
+            // on screen. And use that to set up navigation so that swiping left focuses the view
+            // on the left and swiping right focuses view on the right.
+            View prevChild = null;
+            for (View childView : getChildViewsInOnScreenOrder()) {
+                childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+                childView.setFocusable(true);
+                final View finalPrevChild = prevChild;
+                // Always need to set a new delegate to clear out any previous.
+                childView.setAccessibilityDelegate(new AccessibilityDelegate() {
+                    @Override
+                    public void onInitializeAccessibilityNodeInfo(View host,
+                            AccessibilityNodeInfo info) {
+                        super.onInitializeAccessibilityNodeInfo(host, info);
+                        if (finalPrevChild != null) {
+                            info.setTraversalAfter(finalPrevChild);
+                        }
+                    }
+                });
+                prevChild = childView;
+            }
         } else {
             // Bar is collapsed, only focus on the bar
             setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-            childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_NO;
-        }
-        for (int i = 0; i < getChildCount(); i++) {
-            getChildAt(i).setImportantForAccessibility(childA11y);
-            // Only allowing focusing on bubbles when bar is expanded. Otherwise, in talkback mode,
-            // bubbles can be navigates to in collapsed mode.
-            getChildAt(i).setFocusable(mIsBarExpanded);
+            for (int i = 0; i < getChildCount(); i++) {
+                View childView = getChildAt(i);
+                childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+                childView.setFocusable(false);
+            }
         }
     }
 
@@ -1336,7 +1419,7 @@
         CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : "";
 
         // Don't count overflow if it exists
-        int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0);
+        int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0);
         if (bubbleCount > 1) {
             contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles,
                     contentDesc, bubbleCount - 1);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 9270f68..df33df8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.taskbar.TaskbarInsetsController;
 import com.android.launcher3.taskbar.TaskbarStashController;
 import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.SystemUiProxy;
@@ -71,9 +72,11 @@
     private BubbleDragController mBubbleDragController;
     private TaskbarStashController mTaskbarStashController;
     private TaskbarInsetsController mTaskbarInsetsController;
+    private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider;
     private View.OnClickListener mBubbleClickListener;
     private View.OnClickListener mBubbleBarClickListener;
     private BubbleView.Controller mBubbleViewController;
+    private BubbleBarOverflow mOverflowBubble;
 
     // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
     private final MultiValueAlpha mBubbleBarAlpha;
@@ -90,9 +93,11 @@
     private boolean mHiddenForNoBubbles = true;
     private boolean mShouldShowEducation;
 
+    public boolean mOverflowAdded;
+
     private BubbleBarViewAnimator mBubbleBarViewAnimator;
 
-    private TimeSource mTimeSource = System::currentTimeMillis;
+    private final TimeSource mTimeSource = System::currentTimeMillis;
 
     @Nullable
     private BubbleBarBoundsChangeListener mBoundsChangeListener;
@@ -106,13 +111,16 @@
                 R.dimen.bubblebar_icon_size);
     }
 
-    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+    /** Initializes controller. */
+    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers,
+            TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) {
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
         mBubbleDragController = bubbleControllers.bubbleDragController;
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
         mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
+        mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
         onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
                 dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
@@ -120,6 +128,7 @@
         mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
         mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
+        mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -298,7 +307,8 @@
 
     /** Whether a new bubble is animating. */
     public boolean isAnimatingNewBubble() {
-        return mBarView.isAnimatingNewBubble();
+        return mBarView.isAnimatingNewBubble()
+                || (mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimatingBubble());
     }
 
     /** The horizontal margin of the bubble bar from the edge of the screen. */
@@ -342,6 +352,7 @@
             if (hidden) {
                 mBarView.setAlpha(0);
                 mBarView.setExpanded(false);
+                updatePersistentTaskbar(/* isBubbleBarExpanded = */ false);
             }
             mActivity.bubbleBarVisibilityChanged(!hidden);
         }
@@ -386,7 +397,8 @@
         int newIconSize;
         int newPadding;
         Resources res = mActivity.getResources();
-        if (mBubbleStashController.isBubblesShowingOnHome()) {
+        if (mBubbleStashController.isBubblesShowingOnHome()
+                || mBubbleStashController.isTransientTaskBar()) {
             newIconSize = getBubbleBarIconSizeFromDeviceProfile(res);
             newPadding = getBubbleBarPaddingFromDeviceProfile(res);
         } else {
@@ -489,6 +501,46 @@
         }
     }
 
+    /** Whether the overflow view is added to the bubble bar. */
+    public boolean isOverflowAdded() {
+        return mOverflowAdded;
+    }
+
+    /** Shows or hides the overflow view. */
+    public void showOverflow(boolean showOverflow) {
+        if (mOverflowAdded == showOverflow) return;
+        mOverflowAdded = showOverflow;
+        if (mOverflowAdded) {
+            mBarView.addBubble(mOverflowBubble.getView());
+            mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
+            mOverflowBubble.getView().setController(mBubbleViewController);
+        } else {
+            mBarView.removeBubble(mOverflowBubble.getView());
+            mOverflowBubble.getView().setOnClickListener(null);
+            mOverflowBubble.getView().setController(null);
+        }
+    }
+
+    /** Adds the overflow view to the bubble bar while animating a view away. */
+    public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) {
+        if (mOverflowAdded) return;
+        mOverflowAdded = true;
+        mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView());
+        mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
+        mOverflowBubble.getView().setController(mBubbleViewController);
+        removedBubble.getView().setController(null);
+    }
+
+    /** Removes the overflow view to the bubble bar while animating a view in. */
+    public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) {
+        if (!mOverflowAdded) return;
+        mOverflowAdded = false;
+        mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView());
+        addedBubble.getView().setOnClickListener(mBubbleClickListener);
+        addedBubble.getView().setController(mBubbleViewController);
+        mOverflowBubble.getView().setController(null);
+    }
+
     /**
      * Adds the provided bubble to the bubble bar.
      */
@@ -499,15 +551,12 @@
             mBubbleDragController.setupBubbleView(b.getView());
             b.getView().setController(mBubbleViewController);
 
-            if (b instanceof BubbleBarOverflow) {
-                return;
-            }
-
             if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) {
                 // the bubble bar and handle are initialized as part of the first bubble animation.
                 // if the animation is suppressed, immediately stash or show the bubble bar to
                 // ensure they've been initialized.
-                if (mTaskbarStashController.isInApp()) {
+                if (mTaskbarStashController.isInApp()
+                        && mBubbleStashController.isTransientTaskBar()) {
                     mBubbleStashController.stashBubbleBarImmediate();
                 } else {
                     mBubbleStashController.showBubbleBarImmediate();
@@ -530,15 +579,16 @@
             mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
             return;
         }
-
-        if (mBubbleStashController.isBubblesShowingOnHome() && !isExpanding && !isExpanded()) {
-            mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble);
+        boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome()
+                || !mBubbleStashController.isTransientTaskBar();
+        if (persistentTaskbarOrOnHome && !isExpanded()) {
+            mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
             return;
         }
 
-        // only animate the new bubble if we're in an app and not auto expanding
-        if (isInApp && !isExpanding && !isExpanded()) {
-            mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
+        // only animate the new bubble if we're in an app, have handle view and not auto expanding
+        if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) {
+            mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
         }
     }
 
@@ -567,6 +617,7 @@
     public void setExpanded(boolean isExpanded) {
         if (isExpanded != mBarView.isExpanded()) {
             mBarView.setExpanded(isExpanded);
+            updatePersistentTaskbar(isExpanded);
             if (!isExpanded) {
                 mSystemUiProxy.collapseBubbles();
             } else {
@@ -577,11 +628,34 @@
         }
     }
 
+    private void updatePersistentTaskbar(boolean isBubbleBarExpanded) {
+        if (mBubbleStashController.isTransientTaskBar()) return;
+        boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
+        mTaskbarViewPropertiesProvider
+                .getIconsAlpha()
+                .animateToValue(hideTaskbar ? 0 : 1)
+                .start();
+    }
+
+    /** Return {@code true} if expanded bubble bar would intersect the taskbar. */
+    public boolean isIntersectingTaskbar() {
+        if (mBarView.isExpanding() || mBarView.isExpanded()) {
+            Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds();
+            return mBarView.getBubbleBarExpandedBounds().intersect(taskbarViewBounds);
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Sets whether the bubble bar should be expanded. This method is used in response to UI events
      * from SystemUI.
      */
     public void setExpandedFromSysui(boolean isExpanded) {
+        if (isAnimatingNewBubble() && isExpanded) {
+            mBubbleBarViewAnimator.expandedWhileAnimating();
+            return;
+        }
         if (!isExpanded) {
             mBubbleStashController.stashBubbleBar();
         } else {
@@ -598,6 +672,7 @@
     /**
      * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
      * that a bubble is being dragged to dismiss.
+     *
      * @param bubbleView dragged bubble view
      */
     public void onBubbleDragStart(@NonNull BubbleView bubbleView) {
@@ -705,4 +780,14 @@
             pw.println("  Bubble bar view is null!");
         }
     }
+
+    /** Interface for BubbleBarViewController to get the taskbar view properties. */
+    public interface TaskbarViewPropertiesProvider {
+
+        /** Returns the bounds of the taskbar. */
+        Rect getTaskbarViewBounds();
+
+        /** Returns taskbar icons alpha */
+        MultiPropertyFactory<View>.MultiProperty getIconsAlpha();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 03140fe..e00916a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -15,24 +15,32 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
+import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_BUBBLE_BAR;
+
+import android.graphics.Rect;
+import android.view.View;
+
 import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
+import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.RunnableList;
 
 import java.io.PrintWriter;
+import java.util.Optional;
 
-/**
- * Hosts various bubble controllers to facilitate passing between one another.
- */
+/** Hosts various bubble controllers to facilitate passing between one another. */
 public class BubbleControllers {
 
     public final BubbleBarController bubbleBarController;
     public final BubbleBarViewController bubbleBarViewController;
     public final BubbleStashController bubbleStashController;
-    public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
+    public final Optional<BubbleStashedHandleViewController> bubbleStashedHandleViewController;
     public final BubbleDragController bubbleDragController;
     public final BubbleDismissController bubbleDismissController;
     public final BubbleBarPinController bubbleBarPinController;
     public final BubblePinController bubblePinController;
+    public final BubbleCreator bubbleCreator;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
 
@@ -45,11 +53,12 @@
             BubbleBarController bubbleBarController,
             BubbleBarViewController bubbleBarViewController,
             BubbleStashController bubbleStashController,
-            BubbleStashedHandleViewController bubbleStashedHandleViewController,
+            Optional<BubbleStashedHandleViewController> bubbleStashedHandleViewController,
             BubbleDragController bubbleDragController,
             BubbleDismissController bubbleDismissController,
             BubbleBarPinController bubbleBarPinController,
-            BubblePinController bubblePinController) {
+            BubblePinController bubblePinController,
+            BubbleCreator bubbleCreator) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
         this.bubbleStashController = bubbleStashController;
@@ -58,6 +67,7 @@
         this.bubbleDismissController = bubbleDismissController;
         this.bubbleBarPinController = bubbleBarPinController;
         this.bubblePinController = bubblePinController;
+        this.bubbleCreator = bubbleCreator;
     }
 
     /**
@@ -68,9 +78,28 @@
     public void init(TaskbarControllers taskbarControllers) {
         bubbleBarController.init(this,
                 taskbarControllers.navbarButtonsViewController::isImeVisible);
-        bubbleBarViewController.init(taskbarControllers, this);
-        bubbleStashedHandleViewController.init(taskbarControllers, this);
-        bubbleStashController.init(taskbarControllers, this);
+        bubbleStashedHandleViewController.ifPresent(
+                controller -> controller.init(/* bubbleControllers = */ this));
+        bubbleStashController.init(
+                taskbarControllers.taskbarInsetsController,
+                bubbleBarViewController,
+                bubbleStashedHandleViewController.orElse(null),
+                taskbarControllers::runAfterInit
+        );
+        bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this,
+                new TaskbarViewPropertiesProvider() {
+                    @Override
+                    public Rect getTaskbarViewBounds() {
+                        return taskbarControllers.taskbarViewController.getIconLayoutBounds();
+                    }
+
+                    @Override
+                    public MultiPropertyFactory<View>.MultiProperty getIconsAlpha() {
+                        return taskbarControllers.taskbarViewController
+                                .getTaskbarIconAlpha()
+                                .get(ALPHA_INDEX_BUBBLE_BAR);
+                    }
+                });
         bubbleDragController.init(/* bubbleControllers = */ this);
         bubbleDismissController.init(/* bubbleControllers = */ this);
         bubbleBarPinController.init(this);
@@ -93,7 +122,7 @@
      * Cleans up all controllers.
      */
     public void onDestroy() {
-        bubbleStashedHandleViewController.onDestroy();
+        bubbleStashedHandleViewController.ifPresent(BubbleStashedHandleViewController::onDestroy);
         bubbleBarController.onDestroy();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
new file mode 100644
index 0000000..8e9a2f6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles;
+
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
+
+import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.PathParser;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.appcompat.content.res.AppCompatResources;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.BubbleIconFactory;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.wm.shell.common.bubbles.BubbleInfo;
+
+/**
+ * Loads the necessary info to populate / present a bubble (name, icon, shortcut).
+ */
+public class BubbleCreator {
+
+    private static final String TAG = BubbleCreator.class.getSimpleName();
+
+    private final Context mContext;
+    private final LauncherApps mLauncherApps;
+    private final BubbleIconFactory mIconFactory;
+
+    public BubbleCreator(Context context) {
+        mContext = context;
+        mLauncherApps = mContext.getSystemService(LauncherApps.class);
+        mIconFactory = new BubbleIconFactory(context,
+                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
+                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
+                context.getResources().getColor(R.color.important_conversation),
+                context.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.importance_ring_stroke_width));
+    }
+
+    /**
+     * Creates a BubbleBarBubble object, including the view if needed, and populates it with
+     * the info needed for presentation.
+     *
+     * @param context the context to use for inflation.
+     * @param info the info to use to populate the bubble.
+     * @param barView the parent view for the bubble (bubble is not added to the view).
+     * @param existingBubble if a bubble exists already, this object gets updated with the new
+     *                       info & returned (& any existing views are reused instead of inflating
+     *                       new ones.
+     */
+    @Nullable
+    public BubbleBarBubble populateBubble(Context context, BubbleInfo info, ViewGroup barView,
+            @Nullable BubbleBarBubble existingBubble) {
+        String appName;
+        Bitmap badgeBitmap;
+        Bitmap bubbleBitmap;
+        Path dotPath;
+        int dotColor;
+
+        boolean isImportantConvo = info.isImportantConversation();
+
+        ShortcutRequest.QueryResult result = new ShortcutRequest(context,
+                new UserHandle(info.getUserId()))
+                .forPackage(info.getPackageName(), info.getShortcutId())
+                .query(FLAG_MATCH_DYNAMIC
+                        | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
+                        | FLAG_MATCH_CACHED
+                        | FLAG_GET_PERSONS_DATA);
+
+        ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
+        if (shortcutInfo == null) {
+            Log.w(TAG, "No shortcutInfo found for bubble: " + info.getKey()
+                    + " with shortcutId: " + info.getShortcutId());
+        }
+
+        ApplicationInfo appInfo;
+        try {
+            appInfo = mLauncherApps.getApplicationInfo(
+                    info.getPackageName(),
+                    0,
+                    new UserHandle(info.getUserId()));
+        } catch (PackageManager.NameNotFoundException e) {
+            // If we can't find package... don't think we should show the bubble.
+            Log.w(TAG, "Unable to find packageName: " + info.getPackageName());
+            return null;
+        }
+        if (appInfo == null) {
+            Log.w(TAG, "Unable to find appInfo: " + info.getPackageName());
+            return null;
+        }
+        PackageManager pm = context.getPackageManager();
+        appName = String.valueOf(appInfo.loadLabel(pm));
+        Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
+        Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(info.getUserId()));
+
+        // Badged bubble image
+        Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
+                info.getIcon());
+        if (bubbleDrawable == null) {
+            // Default to app icon
+            bubbleDrawable = appIcon;
+        }
+
+        BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
+        badgeBitmap = badgeBitmapInfo.icon;
+
+        float[] bubbleBitmapScale = new float[1];
+        bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
+
+        // Dot color & placement
+        Path iconPath = PathParser.createPathFromPathData(
+                context.getResources().getString(
+                        com.android.internal.R.string.config_icon_mask));
+        Matrix matrix = new Matrix();
+        float scale = bubbleBitmapScale[0];
+        float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
+        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+                radius /* pivot y */);
+        iconPath.transform(matrix);
+        dotPath = iconPath;
+        dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
+                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
+
+        if (existingBubble == null) {
+            LayoutInflater inflater = LayoutInflater.from(context);
+            BubbleView bubbleView = (BubbleView) inflater.inflate(
+                    R.layout.bubblebar_item_view, barView, false /* attachToRoot */);
+
+            BubbleBarBubble bubble = new BubbleBarBubble(info, bubbleView,
+                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
+            bubbleView.setBubble(bubble);
+            return bubble;
+        } else {
+            // If we already have a bubble (so it already has an inflated view), update it.
+            existingBubble.setInfo(info);
+            existingBubble.setBadge(badgeBitmap);
+            existingBubble.setIcon(bubbleBitmap);
+            existingBubble.setDotColor(dotColor);
+            existingBubble.setDotPath(dotPath);
+            existingBubble.setAppName(appName);
+            return existingBubble;
+        }
+    }
+
+    /**
+     * Creates the overflow view shown in the bubble bar.
+     *
+     * @param barView the parent view for the bubble (bubble is not added to the view).
+     */
+    public BubbleBarOverflow createOverflow(ViewGroup barView) {
+        Bitmap bitmap = createOverflowBitmap();
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        BubbleView bubbleView = (BubbleView) inflater.inflate(
+                R.layout.bubble_bar_overflow_button, barView, false /* attachToRoot */);
+        BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
+        bubbleView.setOverflow(overflow, bitmap);
+        return overflow;
+    }
+
+    private Bitmap createOverflowBitmap() {
+        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
+                R.drawable.bubble_ic_overflow_button);
+
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[]{
+                        R.attr.materialColorOnPrimaryFixed,
+                        R.attr.materialColorPrimaryFixed
+                });
+        int overflowIconColor = ta.getColor(0, Color.WHITE);
+        int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
+        ta.recycle();
+
+        iconDrawable.setTint(overflowIconColor);
+
+        int inset = mContext.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
+        Drawable foreground = new InsetDrawable(iconDrawable, inset);
+        Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
+                foreground);
+
+        return mIconFactory.createBadgedIconBitmap(drawable).icon;
+    }
+
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
index a77e685..1341b53 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.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.stashing.BubbleStashController
 import com.android.wm.shell.common.bubbles.BaseBubblePinController
 import com.android.wm.shell.common.bubbles.BubbleBarLocation
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
deleted file mode 100644
index 74f58ac..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.taskbar.bubbles;
-
-import static java.lang.Math.abs;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.annotation.Nullable;
-import android.view.InsetsController;
-import android.view.MotionEvent;
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.taskbar.StashedHandleViewController;
-import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.launcher3.taskbar.TaskbarControllers;
-import com.android.launcher3.taskbar.TaskbarInsetsController;
-import com.android.launcher3.taskbar.TaskbarStashController;
-import com.android.launcher3.util.MultiPropertyFactory;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
-import com.android.wm.shell.shared.animation.PhysicsAnimator;
-
-import java.io.PrintWriter;
-
-/**
- * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to
- * create a cohesive animation between stashed/unstashed states.
- */
-public class BubbleStashController {
-
-    private static final String TAG = "BubbleStashController";
-
-    /**
-     * How long to stash/unstash.
-     */
-    public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
-
-    /**
-     * The scale bubble bar animates to when being stashed.
-     */
-    private static final float STASHED_BAR_SCALE = 0.5f;
-
-    protected final TaskbarActivityContext mActivity;
-
-    // Initialized in init.
-    private TaskbarControllers mControllers;
-    private TaskbarInsetsController mTaskbarInsetsController;
-    private BubbleBarViewController mBarViewController;
-    private BubbleStashedHandleViewController mHandleViewController;
-    private TaskbarStashController mTaskbarStashController;
-
-    private MultiPropertyFactory.MultiProperty mIconAlphaForStash;
-    private AnimatedFloat mIconScaleForStash;
-    private AnimatedFloat mIconTranslationYForStash;
-    private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha;
-
-    private boolean mRequestedStashState;
-    private boolean mRequestedExpandedState;
-
-    private boolean mIsStashed;
-    private int mStashedHeight;
-    private int mUnstashedHeight;
-    private boolean mBubblesShowingOnHome;
-    private boolean mBubblesShowingOnOverview;
-    private boolean mIsSysuiLocked;
-
-    private final float mHandleCenterFromScreenBottom;
-
-    @Nullable
-    private AnimatorSet mAnimator;
-
-    public BubbleStashController(TaskbarActivityContext activity) {
-        mActivity = activity;
-        // the handle is centered within the stashed taskbar area
-        mHandleCenterFromScreenBottom =
-                mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f;
-    }
-
-    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
-        mControllers = controllers;
-        mTaskbarInsetsController = controllers.taskbarInsetsController;
-        mBarViewController = bubbleControllers.bubbleBarViewController;
-        mHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
-        mTaskbarStashController = controllers.taskbarStashController;
-
-        mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0);
-        mIconScaleForStash = mBarViewController.getBubbleBarScale();
-        mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY();
-
-        mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get(
-                StashedHandleViewController.ALPHA_INDEX_STASHED);
-
-        mStashedHeight = mHandleViewController.getStashedHeight();
-        mUnstashedHeight = mHandleViewController.getUnstashedHeight();
-    }
-
-    /**
-     * Returns the touchable height of the bubble bar based on it's stashed state.
-     */
-    public int getTouchableHeight() {
-        return mIsStashed ? mStashedHeight : mUnstashedHeight;
-    }
-
-    /**
-     * Returns whether the bubble bar is currently stashed.
-     */
-    public boolean isStashed() {
-        return mIsStashed;
-    }
-
-    /**
-     * Animates the handle (or bubble bar depending on state) to be visible after the device is
-     * unlocked.
-     *
-     * <p>Normally either the bubble bar or the handle is visible,
-     * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
-     * between these two states. But the transition from the state where both the bar and handle
-     * are invisible is slightly different.
-     */
-    private void animateAfterUnlock() {
-        AnimatorSet animatorSet = new AnimatorSet();
-        if (mBubblesShowingOnHome || mBubblesShowingOnOverview) {
-            mIsStashed = false;
-            animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
-                    mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
-                    mIconAlphaForStash.animateToValue(1));
-        } else {
-            mIsStashed = true;
-            animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1));
-        }
-
-        animatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                onIsStashedChanged();
-            }
-        });
-        animatorSet.setDuration(BAR_STASH_DURATION).start();
-    }
-
-    /**
-     * Called when launcher enters or exits the home page. Bubbles are unstashed on home.
-     */
-    public void setBubblesShowingOnHome(boolean onHome) {
-        if (mBubblesShowingOnHome != onHome) {
-            mBubblesShowingOnHome = onHome;
-
-            if (!mBarViewController.hasBubbles()) {
-                // if there are no bubbles, there's nothing to show, so just return.
-                return;
-            }
-
-            if (mBubblesShowingOnHome) {
-                showBubbleBar(/* expanded= */ false);
-                // When transitioning from app to home the stash animator may already have been
-                // created, so we need to animate the bubble bar here to align with hotseat.
-                if (!mIsStashed) {
-                    mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat())
-                            .start();
-                }
-                // If the bubble bar is already unstashed, the taskbar touchable region won't be
-                // updated correctly, so force an update here.
-                mControllers.runAfterInit(() ->
-                        mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged());
-            } else if (!mBarViewController.isExpanded()) {
-                stashBubbleBar();
-            }
-        }
-    }
-
-    /** Whether bubbles are showing on the launcher home page. */
-    public boolean isBubblesShowingOnHome() {
-        boolean hasBubbles = mBarViewController != null && mBarViewController.hasBubbles();
-        return mBubblesShowingOnHome && hasBubbles;
-    }
-
-    // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing
-    /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */
-    public void setBubblesShowingOnOverview(boolean onOverview) {
-        if (mBubblesShowingOnOverview != onOverview) {
-            mBubblesShowingOnOverview = onOverview;
-            if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) {
-                stashBubbleBar();
-            } else {
-                // When transitioning to overview the stash animator may already have been
-                // created, so we need to animate the bubble bar here to align with taskbar.
-                mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar())
-                        .start();
-            }
-        }
-    }
-
-    /** Whether bubbles are showing on Overview. */
-    public boolean isBubblesShowingOnOverview() {
-        return mBubblesShowingOnOverview;
-    }
-
-    /** Called when sysui locked state changes, when locked, bubble bar is stashed. */
-    public void onSysuiLockedStateChange(boolean isSysuiLocked) {
-        if (isSysuiLocked != mIsSysuiLocked) {
-            mIsSysuiLocked = isSysuiLocked;
-            if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
-                animateAfterUnlock();
-            }
-        }
-    }
-
-    /**
-     * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the
-     * bar does not stash).
-     */
-    public void stashBubbleBar() {
-        mRequestedStashState = true;
-        mRequestedExpandedState = false;
-        updateStashedAndExpandedState();
-    }
-
-    /**
-     * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}.
-     */
-    public void showBubbleBar(boolean expandBubbles) {
-        mRequestedStashState = false;
-        mRequestedExpandedState = expandBubbles;
-        updateStashedAndExpandedState();
-    }
-
-    private void updateStashedAndExpandedState() {
-        if (mBarViewController.isHiddenForNoBubbles()) {
-            // If there are no bubbles the bar and handle are invisible, nothing to do here.
-            return;
-        }
-        boolean isStashed = mRequestedStashState
-                && !mBubblesShowingOnHome
-                && !mBubblesShowingOnOverview;
-        if (mIsStashed != isStashed) {
-            // notify the view controller that the stash state is about to change so that it can
-            // cancel an ongoing animation if there is one.
-            // note that this has to be called before updating mIsStashed with the new value,
-            // otherwise interrupting an ongoing animation may update it again with the wrong state
-            mBarViewController.onStashStateChanging();
-            mIsStashed = isStashed;
-            if (mAnimator != null) {
-                mAnimator.cancel();
-            }
-            mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION);
-            mAnimator.start();
-            onIsStashedChanged();
-        }
-        if (mBarViewController.isExpanded() != mRequestedExpandedState) {
-            mBarViewController.setExpanded(mRequestedExpandedState);
-        }
-    }
-
-    /**
-     * Create a stash animation.
-     *
-     * @param isStashed whether it's a stash animation or an unstash animation
-     * @param duration duration of the animation
-     * @return the animation
-     */
-    private AnimatorSet createStashAnimator(boolean isStashed, long duration) {
-        AnimatorSet animatorSet = new AnimatorSet();
-
-        AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
-        // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
-        AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
-        AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
-
-        final float firstHalfDurationScale;
-        final float secondHalfDurationScale;
-
-        if (isStashed) {
-            firstHalfDurationScale = 0.75f;
-            secondHalfDurationScale = 0.5f;
-
-            fullLengthAnimatorSet.play(
-                    mIconTranslationYForStash.animateToValue(getStashTranslation()));
-
-            firstHalfAnimatorSet.playTogether(
-                    mIconAlphaForStash.animateToValue(0),
-                    mIconScaleForStash.animateToValue(STASHED_BAR_SCALE));
-            secondHalfAnimatorSet.playTogether(
-                    mBubbleStashedHandleAlpha.animateToValue(1));
-        } else  {
-            firstHalfDurationScale = 0.5f;
-            secondHalfDurationScale = 0.75f;
-
-            final float translationY = getBubbleBarTranslationY();
-
-            fullLengthAnimatorSet.playTogether(
-                    mIconScaleForStash.animateToValue(1),
-                    mIconTranslationYForStash.animateToValue(translationY));
-
-            firstHalfAnimatorSet.playTogether(
-                    mBubbleStashedHandleAlpha.animateToValue(0)
-            );
-            secondHalfAnimatorSet.playTogether(
-                    mIconAlphaForStash.animateToValue(1)
-            );
-        }
-
-        fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed));
-
-        fullLengthAnimatorSet.setDuration(duration);
-        firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
-        secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
-        secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
-
-        animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
-                secondHalfAnimatorSet);
-        animatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimator = null;
-                mControllers.runAfterInit(() -> {
-                    if (isStashed) {
-                        mBarViewController.setExpanded(false);
-                    }
-                    mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
-                });
-            }
-        });
-        return animatorSet;
-    }
-
-    private float getStashTranslation() {
-        return (mUnstashedHeight - mStashedHeight) / 2f;
-    }
-
-    private void onIsStashedChanged() {
-        mControllers.runAfterInit(() -> {
-            mHandleViewController.onIsStashedChanged();
-            mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
-        });
-    }
-
-    public float getBubbleBarTranslationYForTaskbar() {
-        return -mActivity.getDeviceProfile().taskbarBottomMargin;
-    }
-
-    private float getBubbleBarTranslationYForHotseat() {
-        final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx;
-        final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx;
-        return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs(
-                hotseatCellHeight - mUnstashedHeight) / 2;
-    }
-
-    public float getBubbleBarTranslationY() {
-        // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
-        // Otherwise we're either showing in an app or in overview. In either case adjust it so
-        // the bubble bar aligns with the taskbar.
-        return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
-                : getBubbleBarTranslationYForTaskbar();
-    }
-
-    /**
-     * The difference on the Y axis between the center of the handle and the center of the bubble
-     * bar.
-     */
-    public float getDiffBetweenHandleAndBarCenters() {
-        // the difference between the centers of the handle and the bubble bar is the difference
-        // between their distance from the bottom of the screen.
-
-        float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f;
-        return mHandleCenterFromScreenBottom - barCenter;
-    }
-
-    /** The distance the handle moves as part of the new bubble animation. */
-    public float getStashedHandleTranslationForNewBubbleAnimation() {
-        // the should move up to the top of the stashed taskbar area. it is centered within it so
-        // it should move the same distance as it is away from the bottom.
-        return -mHandleCenterFromScreenBottom;
-    }
-
-    /** Checks whether the motion event is over the stash handle. */
-    public boolean isEventOverStashHandle(MotionEvent ev) {
-        return mHandleViewController.isEventOverHandle(ev);
-    }
-
-    /** Set a bubble bar location */
-    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
-        mHandleViewController.setBubbleBarLocation(bubbleBarLocation);
-    }
-
-    /** Returns the [PhysicsAnimator] for the stashed handle view. */
-    public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
-        return mHandleViewController.getPhysicsAnimator();
-    }
-
-    /** Notifies taskbar that it should update its touchable region. */
-    public void updateTaskbarTouchRegion() {
-        mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
-    }
-
-    /** Shows the bubble bar immediately without animation. */
-    public void showBubbleBarImmediate() {
-        mHandleViewController.setTranslationYForSwipe(0);
-        mIconTranslationYForStash.updateValue(getBubbleBarTranslationY());
-        mIconAlphaForStash.setValue(1);
-        mIconScaleForStash.updateValue(1);
-        mIsStashed = false;
-        onIsStashedChanged();
-    }
-
-    /** Stashes the bubble bar immediately without animation. */
-    public void stashBubbleBarImmediate() {
-        mHandleViewController.setTranslationYForSwipe(0);
-        mBubbleStashedHandleAlpha.setValue(1);
-        mIconAlphaForStash.setValue(0);
-        mIconTranslationYForStash.updateValue(getStashTranslation());
-        mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
-        mIsStashed = true;
-        onIsStashedChanged();
-    }
-
-    /**
-     * Updates the values of the internal animators after the new bubble animation was interrupted
-     *
-     * @param isStashed whether the current state should be stashed
-     * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
-     *                              bubble bar is showing to ensure that the stash animator runs
-     *                              smoothly.
-     */
-    public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) {
-        if (isStashed) {
-            mBubbleStashedHandleAlpha.setValue(1);
-            mIconAlphaForStash.setValue(0);
-            mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
-            mIconTranslationYForStash.updateValue(getStashTranslation());
-        } else {
-            mBubbleStashedHandleAlpha.setValue(0);
-            mHandleViewController.setTranslationYForSwipe(0);
-            mIconAlphaForStash.setValue(1);
-            mIconScaleForStash.updateValue(1);
-            mIconTranslationYForStash.updateValue(bubbleBarTranslationY);
-        }
-        mIsStashed = isStashed;
-        onIsStashedChanged();
-    }
-
-    /** Set the translation Y for the stashed handle. */
-    public void setHandleTranslationY(float ty) {
-        mHandleViewController.setTranslationYForSwipe(ty);
-    }
-
-    /** Dumps the state of BubbleStashController. */
-    public void dump(PrintWriter pw) {
-        pw.println("Bubble stash controller state:");
-        pw.println("  mIsStashed: " + mIsStashed);
-        pw.println("  mBubblesShowingOnOverview: " + mBubblesShowingOnOverview);
-        pw.println("  mBubblesShowingOnHome: " + mBubblesShowingOnHome);
-        pw.println("  mIsSysuiLocked: " + mIsSysuiLocked);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 91103d7..8158fe7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -33,7 +33,7 @@
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.taskbar.StashedHandleView;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -79,7 +79,8 @@
         mStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1);
     }
 
-    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+    /** Initialize controller. */
+    public void init(BubbleControllers bubbleControllers) {
         mBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleStashController = bubbleControllers.bubbleStashController;
 
@@ -117,7 +118,7 @@
                     public Rect getSampledRegion(View sampledView) {
                         return mStashedHandleView.getSampledRegion();
                     }
-                }, Executors.UI_HELPER_EXECUTOR);
+                }, Executors.MAIN_EXECUTOR, Executors.UI_HELPER_EXECUTOR);
 
         mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
                 updateBounds(mBarViewController.getBubbleBarLocation()));
@@ -244,6 +245,11 @@
         mStashedHandleView.setTranslationY(transY);
     }
 
+    /** Returns the translation of the stashed handle. */
+    public float getTranslationY() {
+        return mStashedHandleView.getTranslationY();
+    }
+
     /**
      * Used by {@link BubbleStashController} to animate the handle when stashing or un stashing.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 09da3e0..f0f2872 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -74,6 +74,7 @@
     private boolean mOnLeft = false;
 
     private BubbleBarItem mBubble;
+    private boolean mIsOverflow;
 
     private Bitmap mIcon;
 
@@ -271,12 +272,18 @@
      */
     public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
         mBubble = overflow;
+        mIsOverflow = true;
         mIcon = bitmap;
         updateBubbleIcon();
         mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
         setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description));
     }
 
+    /** Whether this view represents the overflow button. */
+    public boolean isOverflow() {
+        return mIsOverflow;
+    }
+
     /** Returns the bubble being rendered in this view. */
     @Nullable
     public BubbleBarItem getBubble() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index feff9fd..b745193 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -26,8 +26,8 @@
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
-import com.android.launcher3.taskbar.bubbles.BubbleStashController
 import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 
 /** Handles animations for bubble bar bubbles. */
@@ -43,6 +43,8 @@
     private val bubbleBarBounceDistanceInPx =
         bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
 
+    fun hasAnimatingBubble() = animatingBubble != null
+
     private companion object {
         /** The time to show the flyout. */
         const val FLYOUT_DELAY_MS: Long = 2500
@@ -58,8 +60,33 @@
     private data class AnimatingBubble(
         val bubbleView: BubbleView,
         val showAnimation: Runnable,
-        val hideAnimation: Runnable
-    )
+        val hideAnimation: Runnable,
+        val expand: Boolean,
+        val state: State = State.CREATED
+    ) {
+
+        /**
+         * The state of the animation.
+         *
+         * The animation is initially created but will be scheduled later using the [Scheduler].
+         *
+         * The normal uninterrupted cycle is for the bubble notification to animate in, then be in a
+         * transient state and eventually to animate out.
+         *
+         * However different events, such as touch and external signals, may cause the animation to
+         * end earlier.
+         */
+        enum class State {
+            /** The animation is created but not started yet. */
+            CREATED,
+            /** The bubble notification is animating in. */
+            ANIMATING_IN,
+            /** The bubble notification is now fully showing and waiting to be hidden. */
+            IN,
+            /** The bubble notification is animating out. */
+            ANIMATING_OUT
+        }
+    }
 
     /** An interface for scheduling jobs. */
     interface Scheduler {
@@ -97,15 +124,18 @@
         )
 
     /** Animates a bubble for the state where the bubble bar is stashed. */
-    fun animateBubbleInForStashed(b: BubbleBarBubble) {
+    fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
+        // TODO b/346400677: handle animations for the same bubble interrupting each other
+        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
         // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
         // and the second part hides it after a delay.
         val showAnimation = buildHandleToBubbleBarAnimation()
-        val hideAnimation = buildBubbleBarToHandleAnimation()
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation()
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
@@ -125,6 +155,7 @@
      *    visible which helps avoiding further updates when we re-enter the second part.
      */
     private fun buildHandleToBubbleBarAnimation() = Runnable {
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
         // prepare the bubble bar for the animation
         bubbleBarView.onAnimatingBubbleStarted()
         bubbleBarView.visibility = VISIBLE
@@ -138,26 +169,29 @@
         // handle. when the handle becomes invisible and we start animating in the bubble bar,
         // the translation y is offset by this value to make the transition from the handle to the
         // bar smooth.
-        val offset = bubbleStashController.diffBetweenHandleAndBarCenters
+        val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
+        val stashedHandleTranslationYForAnimation =
+            bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
         val stashedHandleTranslationY =
-            bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
+            bubbleStashController.getHandleTranslationY() ?: return@Runnable
+        val translationTracker = TranslationTracker(stashedHandleTranslationY)
 
         // this is the total distance that both the stashed handle and the bubble will be traveling
         // at the end of the animation the bubble bar will be positioned in the same place when it
         // shows while we're in an app.
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
-        val animator = bubbleStashController.stashedHandlePhysicsAnimator
+        val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
         animator.addUpdateListener { handle, values ->
             val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
             when {
-                ty >= stashedHandleTranslationY -> {
+                ty >= stashedHandleTranslationYForAnimation -> {
                     // we're in the first leg of the animation. only animate the handle. the bubble
                     // bar remains hidden during this part of the animation
 
                     // map the path [0, stashedHandleTranslationY] to [0,1]
-                    val fraction = ty / stashedHandleTranslationY
+                    val fraction = ty / stashedHandleTranslationYForAnimation
                     handle.alpha = 1 - fraction
                 }
                 ty >= totalTranslationY -> {
@@ -171,8 +205,8 @@
                     if (bubbleBarView.alpha != 1f) {
                         // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1]
                         val fraction =
-                            (ty - stashedHandleTranslationY) /
-                                (totalTranslationY - stashedHandleTranslationY)
+                            (ty - stashedHandleTranslationYForAnimation) /
+                                (totalTranslationY - stashedHandleTranslationYForAnimation)
                         bubbleBarView.alpha = fraction
                         bubbleBarView.scaleY =
                             BUBBLE_ANIMATION_INITIAL_SCALE_Y +
@@ -192,18 +226,16 @@
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
             }
+            translationTracker.updateTyAndExpandIfNeeded(ty)
         }
         animator.addEndListener { _, _, _, canceled, _, _, _ ->
             // if the show animation was canceled, also cancel the hide animation. this is typically
             // canceled in this class, but could potentially be canceled elsewhere.
-            if (canceled) {
-                val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
-                scheduler.cancel(hideAnimation)
-                animatingBubble = null
-                bubbleBarView.onAnimatingBubbleCompleted()
-                bubbleBarView.relativePivotY = 1f
+            if (canceled || animatingBubble?.expand == true) {
+                cancelHideAnimation()
                 return@addEndListener
             }
+            moveToState(AnimatingBubble.State.IN)
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -226,13 +258,14 @@
      */
     private fun buildBubbleBarToHandleAnimation() = Runnable {
         if (animatingBubble == null) return@Runnable
-        val offset = bubbleStashController.diffBetweenHandleAndBarCenters
+        moveToState(AnimatingBubble.State.ANIMATING_OUT)
+        val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
         val stashedHandleTranslationY =
-            bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
+            bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
         // this is the total distance that both the stashed handle and the bar will be traveling
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
         bubbleStashController.setHandleTranslationY(totalTranslationY)
-        val animator = bubbleStashController.stashedHandlePhysicsAnimator
+        val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
         animator.addUpdateListener { handle, values ->
@@ -280,6 +313,8 @@
 
     /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
     fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
+        // TODO b/346400677: handle animations for the same bubble interrupting each other
+        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
@@ -299,12 +334,14 @@
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
             }
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
     private fun buildBubbleBarSpringInAnimation() = Runnable {
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
         // prepare the bubble bar for the animation
         bubbleBarView.onAnimatingBubbleStarted()
         bubbleBarView.translationY = bubbleBarView.height.toFloat()
@@ -313,18 +350,31 @@
         bubbleBarView.scaleX = 1f
         bubbleBarView.scaleY = 1f
 
+        val translationTracker = TranslationTracker(bubbleBarView.translationY)
+
         val animator = PhysicsAnimator.getInstance(bubbleBarView)
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
-        animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() }
+        animator.addUpdateListener { _, values ->
+            val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
+            translationTracker.updateTyAndExpandIfNeeded(ty)
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
         animator.addEndListener { _, _, _, _, _, _, _ ->
+            if (animatingBubble?.expand == true) {
+                cancelHideAnimation()
+            } else {
+                moveToState(AnimatingBubble.State.IN)
+            }
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
         animator.start()
     }
 
-    fun animateBubbleBarForCollapsed(b: BubbleBarBubble) {
+    fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
+        // TODO b/346400677: handle animations for the same bubble interrupting each other
+        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
@@ -335,7 +385,8 @@
             bubbleBarView.onAnimatingBubbleCompleted()
             bubbleStashController.updateTaskbarTouchRegion()
         }
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
@@ -346,24 +397,36 @@
      * the bubble bar moves back to its initial position with a spring animation.
      */
     private fun buildBubbleBarBounceAnimation() = Runnable {
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
         bubbleBarView.onAnimatingBubbleStarted()
         val ty = bubbleStashController.bubbleBarTranslationY
 
         val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
         springBackAnimation.setDefaultSpringConfig(springConfig)
         springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
+        springBackAnimation.addEndListener { _, _, _, _, _, _, _ ->
+            if (animatingBubble?.expand == true) {
+                bubbleBarView.isExpanded = true
+                cancelHideAnimation()
+            } else {
+                moveToState(AnimatingBubble.State.IN)
+            }
+        }
 
         // animate the bubble bar up and start the spring back down animation when it ends.
         ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
             .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
-            .withEndAction { springBackAnimation.start() }
+            .withEndAction {
+                if (animatingBubble?.expand == true) bubbleBarView.isExpanded = true
+                springBackAnimation.start()
+            }
             .start()
     }
 
     /** Handles touching the animating bubble bar. */
     fun onBubbleBarTouchedWhileAnimating() {
         PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
-        bubbleStashController.stashedHandlePhysicsAnimator.cancelIfRunning()
+        bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
         bubbleBarView.onAnimatingBubbleCompleted()
@@ -376,7 +439,7 @@
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
         animatingBubble = null
-        bubbleStashController.stashedHandlePhysicsAnimator.cancel()
+        bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
         bubbleBarView.onAnimatingBubbleCompleted()
         bubbleBarView.relativePivotY = 1f
         bubbleStashController.onNewBubbleAnimationInterrupted(
@@ -385,8 +448,27 @@
         )
     }
 
-    private fun <T> PhysicsAnimator<T>.cancelIfRunning() {
-        if (isRunning()) cancel()
+    fun expandedWhileAnimating() {
+        val animatingBubble = animatingBubble ?: return
+        this.animatingBubble = animatingBubble.copy(expand = true)
+        // if we're fully in and waiting to hide, cancel the hide animation and clean up
+        if (animatingBubble.state == AnimatingBubble.State.IN) {
+            bubbleBarView.isExpanded = true
+            cancelHideAnimation()
+        }
+    }
+
+    private fun cancelHideAnimation() {
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        animatingBubble = null
+        bubbleBarView.onAnimatingBubbleCompleted()
+        bubbleBarView.relativePivotY = 1f
+        bubbleStashController.showBubbleBarImmediate()
+    }
+
+    private fun <T> PhysicsAnimator<T>?.cancelIfRunning() {
+        if (this?.isRunning() == true) cancel()
     }
 
     private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator {
@@ -404,4 +486,37 @@
         )
         return this
     }
+
+    private fun moveToState(state: AnimatingBubble.State) {
+        val animatingBubble = this.animatingBubble ?: return
+        this.animatingBubble = animatingBubble.copy(state = state)
+    }
+
+    /**
+     * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands
+     * as part of the animation, the expansion should start after the bubble bar reaches the peak
+     * position.
+     */
+    private inner class TranslationTracker(initialTy: Float) {
+        private var previousTy = initialTy
+        private var startedExpanding = false
+        private var reachedPeak = false
+
+        fun updateTyAndExpandIfNeeded(ty: Float) {
+            if (!reachedPeak) {
+                // the bubble bar is positioned at the bottom of the screen and moves up using
+                // negative ty values. the peak is reached the first time we see a value that is
+                // greater than the previous.
+                if (ty > previousTy) {
+                    reachedPeak = true
+                }
+            }
+            val expand = animatingBubble?.expand ?: false
+            if (reachedPeak && expand && !startedExpanding) {
+                bubbleBarView.isExpanded = true
+                startedExpanding = true
+            }
+            previousTy = ty
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
index 59fc76c..48eb7de 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -52,7 +52,7 @@
     /** Execute passed action only after controllers are initiated. */
     interface ControllersAfterInitAction {
         /** Execute action after controllers are initiated. */
-        fun runAfterInit(action: () -> Unit)
+        fun runAfterInit(action: Runnable)
     }
 
     /** Whether bubble bar is currently stashed */
@@ -143,6 +143,9 @@
     /** Set the translation Y for the stashed handle. */
     fun setHandleTranslationY(translationY: Float)
 
+    /** Returns the translation of the handle. */
+    fun getHandleTranslationY(): Float?
+
     /**
      * Returns bubble bar Y position according to [isBubblesShowingOnHome] and
      * [isBubblesShowingOnOverview] values. Default implementation only analyse
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
new file mode 100644
index 0000000..a55763b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.stashing
+
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
+
+/**
+ * Implementation of the [TaskbarHotseatDimensionsProvider] that take sizes from the
+ * [DeviceProfile].
+ */
+class DeviceProfileDimensionsProviderAdapter(
+    private val taskbarActivityContext: TaskbarActivityContext
+) : TaskbarHotseatDimensionsProvider {
+    override fun getTaskbarBottomSpace(): Int = deviceProfile().taskbarBottomMargin
+
+    override fun getTaskbarHeight(): Int = deviceProfile().taskbarHeight
+
+    override fun getHotseatBottomSpace(): Int = deviceProfile().hotseatBarBottomSpacePx
+
+    override fun getHotseatHeight(): Int = deviceProfile().hotseatCellHeightPx
+
+    private fun deviceProfile(): DeviceProfile = taskbarActivityContext.deviceProfile
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 62fe221..1b65019 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -198,6 +198,8 @@
         // no op since does not have a handle view
     }
 
+    override fun getHandleTranslationY(): Float? = null
+
     private fun updateExpandedState(expand: Boolean) {
         if (bubbleBarViewController.isHiddenForNoBubbles) {
             // If there are no bubbles the bar is invisible, nothing to do here.
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 23e009b..1a4b982 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -242,6 +242,8 @@
         bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY)
     }
 
+    override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY
+
     private fun getStashTranslation(): Float {
         return (bubbleBarViewController.bubbleBarCollapsedHeight - stashedHeight) / 2f
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
index 415a051..7d2d36d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -47,7 +47,7 @@
 
     override val spaceNeeded: Int
         get() {
-            return dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconSize.size.toFloat())
+            return dpToPx(activityContext.taskbarSpecsEvaluator!!.taskbarIconSize.size.toFloat())
         }
 
     init {
@@ -58,15 +58,15 @@
     private fun setUpIcon() {
         val drawable =
             resources.getDrawable(
-                getAllAppsButton(activityContext.taskbarFeatureEvaluator.isTransient)
+                getAllAppsButton(activityContext.taskbarFeatureEvaluator!!.isTransient)
             )
-        val padding = activityContext.taskbarSpecsEvaluator.taskbarIconPadding
+        val padding = activityContext.taskbarSpecsEvaluator!!.taskbarIconPadding
 
         allAppsButton.setIconDrawable(drawable)
-        allAppsButton.setPadding(/* left= */ padding)
+        allAppsButton.setPadding(padding)
         allAppsButton.setForegroundTint(activityContext.getColor(R.color.all_apps_button_color))
 
-        // TODO(jagrutdesai) : add click listeners in future cl
+        // TODO(b/356465292) : add click listeners in future cl
         addView(allAppsButton)
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
new file mode 100644
index 0000000..26e71f7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.customization
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import androidx.core.view.setPadding
+import com.android.launcher3.R
+import com.android.launcher3.Utilities.dpToPx
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.views.ActivityContext
+import com.android.launcher3.views.IconButtonView
+
+/** Taskbar divider view container for customizable taskbar. */
+class TaskbarDividerContainer
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+) : LinearLayout(context, attrs), TaskbarContainer {
+
+    private val taskbarDivider: IconButtonView =
+        LayoutInflater.from(context).inflate(R.layout.taskbar_divider, this, false)
+            as IconButtonView
+    private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
+
+    override val spaceNeeded: Int
+        get() {
+            return dpToPx(activityContext.taskbarSpecsEvaluator!!.taskbarIconSize.size.toFloat())
+        }
+
+    init {
+        setUpIcon()
+    }
+
+    @SuppressLint("UseCompatLoadingForDrawables")
+    fun setUpIcon() {
+        val drawable = resources.getDrawable(R.drawable.taskbar_divider_button)
+        val padding = activityContext.taskbarSpecsEvaluator!!.taskbarIconPadding
+
+        taskbarDivider.setIconDrawable(drawable)
+        taskbarDivider.setPadding(padding)
+
+        // TODO(b/356465292):: add click listeners in future cl
+        addView(taskbarDivider)
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 5da2df8..6085998 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -45,6 +45,7 @@
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
+import static com.android.launcher3.popup.SystemShortcut.BUBBLE_SHORTCUT;
 import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP;
 import static com.android.launcher3.popup.SystemShortcut.INSTALL;
 import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL;
@@ -67,7 +68,6 @@
 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.common.split.SplitScreenConstants.SNAP_TO_50_50;
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -76,6 +76,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.ShortcutInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -200,6 +201,7 @@
 import com.android.systemui.unfold.dagger.UnfoldMain;
 import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
 import com.android.systemui.unfold.updates.RotationChangeProvider;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import kotlin.Unit;
 
@@ -215,7 +217,8 @@
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
-public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
+public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
+        SystemShortcut.BubbleActivityStarter {
     private static final boolean TRACE_LAYOUTS =
             SystemProperties.getBoolean("persist.debug.trace_layouts", false);
     private static final String TRACE_RELAYOUT_CLASS =
@@ -280,7 +283,7 @@
         // TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
         OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
                 asContext(), deviceState);
-        if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
+        if (DesktopModeStatus.canEnterDesktopMode(this)) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
                     getDepthController());
@@ -300,7 +303,7 @@
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
         mDepthController = new DepthController(this);
-        if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
+        if (DesktopModeStatus.canEnterDesktopMode(this)) {
             mDesktopVisibilityController = new DesktopVisibilityController(this);
             mDesktopVisibilityController.registerSystemUiListener();
             mSplitSelectStateController.initSplitFromDesktopController(this,
@@ -466,6 +469,9 @@
         if (Flags.enablePrivateSpace()) {
             shortcuts.add(UNINSTALL_APP);
         }
+        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+            shortcuts.add(BUBBLE_SHORTCUT);
+        }
         return shortcuts.stream();
     }
 
@@ -1442,6 +1448,18 @@
         return true;
     }
 
+    @Override
+    public void showShortcutBubble(ShortcutInfo info) {
+        if (info == null) return;
+        SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
+    }
+
+    @Override
+    public void showAppBubble(Intent intent) {
+        if (intent == null || intent.getPackage() == null) return;
+        SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<QuickstepLauncher> {
 
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 5a03ae6..4dc04e7 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -62,7 +62,6 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -151,6 +150,7 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
 
 import kotlin.Unit;
@@ -989,7 +989,6 @@
                 dp = dp.copy(mContext);
             }
             dp.updateInsets(targets.homeContentInsets);
-            dp.updateIsSeascape(mContext);
             initTransitionEndpoints(dp);
             orientationState.setMultiWindowMode(dp.isMultiWindowMode);
         }
@@ -1273,7 +1272,7 @@
         TaskView currentPageTaskView = mRecentsView != null
                 ? mRecentsView.getCurrentPageTaskView() : null;
 
-        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)
                 && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
                 && DesktopModeFlags.QUICK_SWITCH.isEnabled(mContext))) {
             if ((nextPageTaskView instanceof DesktopTaskView
@@ -1446,7 +1445,7 @@
             setClampScrollOffset(false);
         };
 
-        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)
                 && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
                 && DesktopModeFlags.QUICK_SWITCH.isEnabled(mContext))) {
             if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
@@ -2100,7 +2099,6 @@
             // If there are no targets, then we don't need to capture anything
             mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         } else {
-            boolean finishTransitionPosted = false;
             // If we already have cached screenshot(s) from running tasks, skip update
             boolean shouldUpdate = false;
             int[] runningTaskIds = mGestureState.getRunningTaskIds(mIsSwipeForSplit);
@@ -2124,45 +2122,32 @@
                         }
 
                         MAIN_EXECUTOR.execute(() -> {
-                            if (!updateThumbnail(false /* refreshView */)) {
-                                setScreenshotCapturedState();
-                            }
+                            updateThumbnail();
+                            setScreenshotCapturedState();
                         });
                     });
                     return;
                 }
 
-                finishTransitionPosted = updateThumbnail(false /* refreshView */);
+                updateThumbnail();
             }
 
-            if (!finishTransitionPosted) {
-                setScreenshotCapturedState();
-            }
+            setScreenshotCapturedState();
         }
     }
 
     // Returns whether finish transition was posted.
-    private boolean updateThumbnail(boolean refreshView) {
+    private void updateThumbnail() {
         if (mGestureState.getEndTarget() == HOME
                 || mGestureState.getEndTarget() == NEW_TASK
                 || mGestureState.getEndTarget() == ALL_APPS
                 || mRecentsView == null) {
             // Capture the screenshot before finishing the transition to home or quickswitching to
             // ensure it's taken in the correct orientation, but no need to update the thumbnail.
-            return false;
+            return;
         }
 
-        boolean finishTransitionPosted = false;
-        TaskView updatedTaskView = mRecentsView.updateThumbnail(mTaskSnapshotCache, refreshView);
-        if (updatedTaskView != null && refreshView && !mCanceled) {
-            // Defer finishing the animation until the next launcher frame with the
-            // new thumbnail
-            finishTransitionPosted = ViewUtils.postFrameDrawn(updatedTaskView,
-                    () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
-                    this::isCanceled);
-        }
-
-        return finishTransitionPosted;
+        mRecentsView.updateThumbnail(mTaskSnapshotCache);
     }
 
     private void setScreenshotCapturedState() {
@@ -2294,7 +2279,7 @@
                     mRecentsAnimationController, mRecentsAnimationTargets);
         });
 
-        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)
                 && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
                         && DesktopModeFlags.QUICK_SWITCH.isEnabled(mContext))) {
             if (mRecentsView.getNextPageTaskView() instanceof DesktopTaskView
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 00cd60b..293944d 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -31,7 +31,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.graphics.Color;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
@@ -133,7 +132,7 @@
     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
         TaskbarUIController controller = getTaskbarController();
         boolean isEventOverBubbleBarStashHandle =
-                controller != null && controller.isEventOverBubbleBarStashHandle(ev);
+                controller != null && controller.isEventOverBubbleBarViews(ev);
         return deviceState.isInDeferredGestureRegion(ev) || deviceState.isImeRenderingNavButtons()
                 || isTrackpadMultiFingerSwipe(ev) || isEventOverBubbleBarStashHandle;
     }
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 95c86fa..e66da52 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -20,7 +20,6 @@
 
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
 
 import android.app.ActivityManager;
@@ -41,6 +40,7 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.recents.IRecentTasksListener;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 
 import java.io.PrintWriter;
@@ -329,9 +329,9 @@
         int numVisibleTasks = 0;
         for (GroupedRecentTaskInfo rawTask : rawTasks) {
             if (rawTask.getType() == TYPE_FREEFORM) {
-                // TYPE_FREEFORM tasks is only created whenDESKTOP_WINDOWING_MODE.isEnabled is true,
+                // TYPE_FREEFORM tasks is only created when desktop mode can be entered,
                 // leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
-                if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)) {
+                if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
                     GroupTask desktopTask = createDesktopTask(rawTask);
                     if (desktopTask != null) {
                         allTasks.add(desktopTask);
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index e84200d..6d5cb4b 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -27,7 +27,6 @@
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
 import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -57,6 +56,7 @@
 import com.android.launcher3.LauncherAnimationRunner;
 import com.android.launcher3.LauncherAnimationRunner.AnimationResult;
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
+import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
@@ -87,6 +87,7 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -139,15 +140,15 @@
                         null /* depthController */, getStatsLogManager(),
                         systemUiProxy, RecentsModel.INSTANCE.get(this),
                         null /*activityBackCallback*/);
+        // Setup root and child views
         inflateRootView(R.layout.fallback_recents_activity);
-        setContentView(getRootView());
-        mDragLayer = findViewById(R.id.drag_layer);
-        mScrimView = findViewById(R.id.scrim_view);
-        mFallbackRecentsView = findViewById(R.id.overview_panel);
-        mActionsView = findViewById(R.id.overview_actions_view);
-        getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
-        mDragLayer.recreateControllers();
-        if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
+        LauncherRootView rootView = getRootView();
+        mDragLayer = rootView.findViewById(R.id.drag_layer);
+        mScrimView = rootView.findViewById(R.id.scrim_view);
+        mFallbackRecentsView = rootView.findViewById(R.id.overview_panel);
+        mActionsView = rootView.findViewById(R.id.overview_actions_view);
+
+        if (DesktopModeStatus.canEnterDesktopMode(this)) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
                     null /* depthController */
@@ -156,6 +157,10 @@
         mFallbackRecentsView.init(mActionsView, mSplitSelectStateController,
                 mDesktopRecentsTransitionController);
 
+        setContentView(rootView);
+        rootView.getSysUiScrim().getSysUIProgress().updateValue(0);
+        mDragLayer.recreateControllers();
+
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index da7a98f..32e2389 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -128,15 +128,11 @@
                     mController::finishAnimationToApp);
         } else {
             RemoteAnimationTarget[] nonAppTargets;
-            if (!TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-                nonAppTargets = mSystemUiProxy.onGoingToRecentsLegacy(appTargets);
-            } else {
-                final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
-                final ArrayList<RemoteAnimationTarget> nonApps = new ArrayList<>();
-                classifyTargets(appTargets, apps, nonApps);
-                appTargets = apps.toArray(new RemoteAnimationTarget[apps.size()]);
-                nonAppTargets = nonApps.toArray(new RemoteAnimationTarget[nonApps.size()]);
-            }
+            final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
+            final ArrayList<RemoteAnimationTarget> nonApps = new ArrayList<>();
+            classifyTargets(appTargets, apps, nonApps);
+            appTargets = apps.toArray(new RemoteAnimationTarget[apps.size()]);
+            nonAppTargets = nonApps.toArray(new RemoteAnimationTarget[nonApps.size()]);
             if (nonAppTargets == null) {
                 nonAppTargets = new RemoteAnimationTarget[0];
             }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index d104911..cf7e499 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -18,14 +18,14 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
-
 import android.app.WindowConfiguration;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.RemoteAnimationTarget;
 
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+
 import java.io.PrintWriter;
 
 /**
@@ -56,7 +56,7 @@
      * @return {@code true} if at least one target app is a desktop task
      */
     public boolean hasDesktopTasks(Context context) {
-        if (!DESKTOP_WINDOWING_MODE.isEnabled(context)) {
+        if (!DesktopModeStatus.canEnterDesktopMode(context)) {
             return false;
         }
         for (RemoteAnimationTarget target : apps) {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index f2db5af..59e9f05 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -23,8 +23,6 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -49,7 +47,6 @@
 import android.view.IRecentsAnimationRunner;
 import android.view.IRemoteAnimationRunner;
 import android.view.MotionEvent;
-import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.IOnBackInvokedCallback;
@@ -96,6 +93,8 @@
 import com.android.wm.shell.recents.IRecentTasks;
 import com.android.wm.shell.recents.IRecentTasksListener;
 import com.android.wm.shell.shared.IShellTransitions;
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.ISplitScreen;
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
 import com.android.wm.shell.splitscreen.ISplitSelectListener;
@@ -894,6 +893,36 @@
         }
     }
 
+    /**
+     * Tells SysUI to show a shortcut bubble.
+     *
+     * @param info the shortcut info used to create or identify the bubble.
+     */
+    public void showShortcutBubble(ShortcutInfo info) {
+        try {
+            if (mBubbles != null) {
+                mBubbles.showShortcutBubble(info);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call show bubble for shortcut");
+        }
+    }
+
+    /**
+     * Tells SysUI to show a bubble of an app.
+     *
+     * @param intent the intent used to create the bubble.
+     */
+    public void showAppBubble(Intent intent) {
+        try {
+            if (mBubbles != null) {
+                mBubbles.showAppBubble(intent);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call show bubble for app");
+        }
+    }
+
     //
     // Splitscreen
     //
@@ -1000,77 +1029,6 @@
         }
     }
 
-    /**
-     * Start multiple tasks in split-screen simultaneously.
-     */
-    public void startTasksWithLegacyTransition(int taskId1, Bundle options1, int taskId2,
-            Bundle options2, @StagePosition int splitPosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startTasksWithLegacyTransition(taskId1, options1, taskId2, options2,
-                        splitPosition, snapPosition, adapter, instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startTasksWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
-    public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1,
-            Bundle options1, int taskId, Bundle options2, @StagePosition int splitPosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startIntentAndTaskWithLegacyTransition(pendingIntent, userId1,
-                        options1, taskId, options2, splitPosition, snapPosition, adapter,
-                        instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startIntentAndTaskWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
-    public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, Bundle options1,
-            int taskId, Bundle options2, @StagePosition int splitPosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startShortcutAndTaskWithLegacyTransition(shortcutInfo, options1,
-                        taskId, options2, splitPosition, snapPosition, adapter, instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startShortcutAndTaskWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
-    /**
-     * Starts a pair of intents or shortcuts in split-screen using legacy transition. Passing a
-     * non-null shortcut info means to start the app as a shortcut.
-     */
-    public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1,
-            @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
-            PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2,
-            @Nullable Bundle options2, @StagePosition int sidePosition,
-            @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSplitScreen.startIntentsWithLegacyTransition(pendingIntent1, userId1,
-                        shortcutInfo1, options1, pendingIntent2, userId2, shortcutInfo2, options2,
-                        sidePosition, snapPosition, adapter, instanceId);
-            } catch (RemoteException e) {
-                Log.w(TAG, splitFailureMessage(
-                        "startIntentsWithLegacyTransition", "RemoteException"), e);
-            }
-        }
-    }
-
     public void startShortcut(String packageName, String shortcutId, int position,
             Bundle options, UserHandle user, InstanceId instanceId) {
         if (mSplitScreen != null) {
@@ -1105,36 +1063,6 @@
         }
     }
 
-    /**
-     * Call this when going to recents so that shell can set-up and provide appropriate leashes
-     * for animation (eg. DividerBar).
-     *
-     * @return RemoteAnimationTargets of windows that need to animate but only exist in shell.
-     */
-    @Nullable
-    public RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) {
-        if (!TaskAnimationManager.ENABLE_SHELL_TRANSITIONS && mSplitScreen != null) {
-            try {
-                return mSplitScreen.onGoingToRecentsLegacy(apps);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call onGoingToRecentsLegacy");
-            }
-        }
-        return null;
-    }
-
-    @Nullable
-    public RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) {
-        if (mSplitScreen != null) {
-            try {
-                return mSplitScreen.onStartingSplitLegacy(apps);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call onStartingSplitLegacy");
-            }
-        }
-        return null;
-    }
-
     //
     // One handed
     //
@@ -1443,9 +1371,8 @@
     }
 
     private boolean shouldEnableRunningTasksForDesktopMode() {
-        // TODO(b/335401172): unify DesktopMode checks in Launcher
-        return DESKTOP_WINDOWING_MODE.isEnabled(mContext)
-                && enableDesktopWindowingTaskbarRunningApps();
+        return DesktopModeStatus.canEnterDesktopMode(mContext)
+                && DesktopModeFlags.TASKBAR_RUNNING_APPS.isEnabled(mContext);
     }
 
     private boolean handleMessageAsync(Message msg) {
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 723aa03..ab80a8c 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -18,6 +18,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 
 import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
+import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
@@ -44,6 +45,7 @@
 import com.android.internal.util.ArrayUtils;
 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.util.SystemUiFlagUtils;
@@ -264,12 +266,7 @@
                     }
                 }
 
-                RemoteAnimationTarget[] nonAppTargets = ENABLE_SHELL_TRANSITIONS
-                        ? null : getSystemUiProxy().onStartingSplitLegacy(
-                                appearedTaskTargets);
-                if (nonAppTargets == null) {
-                    nonAppTargets = new RemoteAnimationTarget[0];
-                }
+                RemoteAnimationTarget[] nonAppTargets = new RemoteAnimationTarget[0];
                 if ((containerInterface.isInLiveTileMode()
                             || mLastGestureState.getEndTarget() == RECENTS
                             || isNonRecentsStartedTasksAppeared(appearedTaskTargets))
@@ -339,6 +336,28 @@
                 options.setTransientLaunch();
             }
             options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
+
+            // Notify taskbar that we should skip reacting to launcher visibility change to
+            // avoid a jumping taskbar.
+            TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+            if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+                taskbarUIController.setSkipLauncherVisibilityChange(true);
+
+                mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+                    @Override
+                    public void onRecentsAnimationCanceled(
+                            @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+
+                    @Override
+                    public void onRecentsAnimationFinished(
+                            @NonNull RecentsAnimationController controller) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+                });
+            }
+
             mRecentsAnimationStartPending = getSystemUiProxy()
                     .startRecentsActivity(intent, options, mCallbacks);
             if (enableHandleDelayedGestureCallbacks()) {
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 6ed05c8..3cf0542 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -28,7 +28,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -48,7 +47,6 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
 
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -63,10 +61,6 @@
 public class TopTaskTracker extends ISplitScreenListener.Stub
         implements TaskStackChangeListener, SafeCloseable {
 
-    private static final String TAG = "TopTaskTracker";
-
-    private static final boolean DEBUG = true;
-
     public static MainThreadInitializedObject<TopTaskTracker> INSTANCE =
             new MainThreadInitializedObject<>(TopTaskTracker::new);
 
@@ -98,19 +92,10 @@
     @Override
     public void onTaskRemoved(int taskId) {
         mOrderedTaskList.removeIf(rto -> rto.taskId == taskId);
-        if (DEBUG) {
-            Log.i(TAG, "onTaskRemoved: taskId=" + taskId);
-        }
     }
 
     @Override
     public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
-        if (!mOrderedTaskList.isEmpty()
-                && mOrderedTaskList.getFirst().taskId != taskInfo.taskId
-                && DEBUG) {
-            Log.i(TAG, "onTaskMovedToFront: (moved taskInfo to front) taskId=" + taskInfo.taskId
-                    + ", baseIntent=" + taskInfo.baseIntent);
-        }
         mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId);
         mOrderedTaskList.addFirst(taskInfo);
 
@@ -121,11 +106,6 @@
             final RunningTaskInfo topTaskOnHomeDisplay = mOrderedTaskList.stream()
                     .filter(rto -> rto.displayId == DEFAULT_DISPLAY).findFirst().orElse(null);
             if (topTaskOnHomeDisplay != null) {
-                if (DEBUG) {
-                    Log.i(TAG, "onTaskMovedToFront: (removing top task on home display) taskId="
-                            + topTaskOnHomeDisplay.taskId
-                            + ", baseIntent=" + topTaskOnHomeDisplay.baseIntent);
-                }
                 mOrderedTaskList.removeIf(rto -> rto.taskId == topTaskOnHomeDisplay.taskId);
                 mOrderedTaskList.addFirst(topTaskOnHomeDisplay);
             }
@@ -139,10 +119,6 @@
                 if (info.taskId != taskInfo.taskId
                         && info.taskId != mMainStagePosition.taskId
                         && info.taskId != mSideStagePosition.taskId) {
-                    if (DEBUG) {
-                        Log.i(TAG, "onTaskMovedToFront: (removing task list overflow) taskId="
-                                + taskInfo.taskId + ", baseIntent=" + taskInfo.baseIntent);
-                    }
                     itr.remove();
                     return;
                 }
@@ -152,9 +128,6 @@
 
     @Override
     public void onStagePositionChanged(@StageType int stage, @StagePosition int position) {
-        if (DEBUG) {
-            Log.i(TAG, "onStagePositionChanged: stage=" + stage + ", position=" + position);
-        }
         if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
             mMainStagePosition.stagePosition = position;
         } else {
@@ -164,10 +137,6 @@
 
     @Override
     public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
-        if (DEBUG) {
-            Log.i(TAG, "onTaskStageChanged: taskId=" + taskId
-                    + ", stage=" + stage + ", visible=" + visible);
-        }
         // If a task is not visible anymore or has been moved to undefined, stop tracking it.
         if (!visible || stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) {
             if (mMainStagePosition.taskId == taskId) {
@@ -187,18 +156,11 @@
 
     @Override
     public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
-        if (DEBUG) {
-            Log.i(TAG, "onActivityPinned: packageName=" + packageName
-                    + ", userId=" + userId + ", stackId=" + stackId);
-        }
         mPinnedTaskId = taskId;
     }
 
     @Override
     public void onActivityUnpinned() {
-        if (DEBUG) {
-            Log.i(TAG, "onActivityUnpinned");
-        }
         mPinnedTaskId = INVALID_TASK_ID;
     }
 
@@ -250,21 +212,6 @@
         return new CachedTaskInfo(tasks);
     }
 
-    public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "TopTaskTracker:");
-
-        writer.println(prefix + "\tmOrderedTaskList=[");
-        for (RunningTaskInfo taskInfo : mOrderedTaskList) {
-            writer.println(prefix + "\t\t(taskId=" + taskInfo.taskId
-                    + "; baseIntent=" + taskInfo.baseIntent
-                    + "; isRunning=" + taskInfo.isRunning + ")");
-        }
-        writer.println(prefix + "\t]");
-        writer.println(prefix + "\tmMainStagePosition=" + mMainStagePosition);
-        writer.println(prefix + "\tmSideStagePosition=" + mSideStagePosition);
-        writer.println(prefix + "\tmPinnedTaskId=" + mPinnedTaskId);
-    }
-
     /**
      * Class to provide information about a task which can be safely cached and do not change
      * during the lifecycle of the task.
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2896979..5c940a3 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -1610,7 +1610,6 @@
         pw.println("\tmConsumer=" + mConsumer.getName());
         ActiveGestureLog.INSTANCE.dump("", pw);
         RecentsModel.INSTANCE.get(this).dump("", pw);
-        TopTaskTracker.INSTANCE.get(this).dump("", pw);
         if (mTaskAnimationManager != null) {
             mTaskAnimationManager.dump("", pw);
         }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 95295b0..9a99d4a 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -321,8 +321,9 @@
         if (controllers == null) {
             return false;
         }
-        if (controllers.bubbleStashController.isStashed()) {
-            return controllers.bubbleStashedHandleViewController.containsX((int) x);
+        if (controllers.bubbleStashController.isStashed()
+                && controllers.bubbleStashedHandleViewController.isPresent()) {
+            return controllers.bubbleStashedHandleViewController.get().containsX((int) x);
         } else {
             Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds();
             return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right;
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index f89888a..54653fa 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -782,7 +782,6 @@
                 (RelativeLayout.LayoutParams) mFakeHotseatView.getLayoutParams();
         if (!mTutorialFragment.isLargeScreen()) {
             DeviceProfile dp = mTutorialFragment.getDeviceProfile();
-            dp.updateIsSeascape(mContext);
 
             hotseatLayoutParams.addRule(dp.isLandscape
                     ? (dp.isSeascape()
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index d5aaed5..4f7a541 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -46,5 +46,5 @@
      * Override [ThumbnailData] with a map of taskId to [ThumbnailData]. The override only applies
      * if the tasks are already visible, and will be invalidated when tasks become invisible.
      */
-    fun setThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>)
+    fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>)
 }
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 0714170..6acc940 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -82,12 +82,15 @@
 
     override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         this.visibleTaskIds.value = visibleTaskIdList.toSet()
-        setThumbnailOverride(thumbnailOverride.value)
+        addOrUpdateThumbnailOverride(emptyMap())
     }
 
-    override fun setThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
+    override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
         this.thumbnailOverride.value =
-            thumbnailOverride.filterKeys(this.visibleTaskIds.value::contains).toMap()
+            this.thumbnailOverride.value
+                .toMutableMap()
+                .apply { putAll(thumbnailOverride) }
+                .filterKeys(this.visibleTaskIds.value::contains)
     }
 
     /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 7205fc8..b1f46a3 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -58,15 +58,15 @@
         recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
     }
 
-    fun setThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
-        recentsTasksRepository.setThumbnailOverride(thumbnailOverride)
+    fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
+        recentsTasksRepository.addOrUpdateThumbnailOverride(thumbnailOverride)
     }
 
     suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>) {
         combine(
                 updatedThumbnails.map {
                     recentsTasksRepository.getThumbnailById(it.key).filter { thumbnailData ->
-                        thumbnailData == it.value
+                        thumbnailData?.snapshotId == it.value.snapshotId
                     }
                 }
             ) {}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index d906bb3..770ec5a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP;
@@ -60,8 +59,6 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Pair;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.RemoteTransition;
@@ -94,13 +91,11 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SplitSelectionListener;
 import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.SplitInstructionsView;
-import com.android.systemui.animation.RemoteAnimationRunnerCompat;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
@@ -276,17 +271,15 @@
                     // Loop through tasks in reverse, since they are ordered with recent tasks last
                     for (int j = taskGroups.size() - 1; j >= 0; j--) {
                         GroupTask groupTask = taskGroups.get(j);
-                        Task task1 = groupTask.task1;
-                        // Don't add duplicate Tasks
-                        if (isInstanceOfComponent(task1, key)
-                                && !Arrays.asList(lastActiveTasks).contains(task1)) {
-                            lastActiveTask = task1;
-                            break;
+                        // Account for desktop cases where there can be N tasks in the group
+                        for (Task task : groupTask.getTasks()) {
+                            if (isInstanceOfComponent(task, key)
+                                    && !Arrays.asList(lastActiveTasks).contains(task)) {
+                                lastActiveTask = task;
+                                break;
+                            }
                         }
-                        Task task2 = groupTask.task2;
-                        if (isInstanceOfComponent(task2, key)
-                                && !Arrays.asList(lastActiveTasks).contains(task2)) {
-                            lastActiveTask = task2;
+                        if (lastActiveTask != null) {
                             break;
                         }
                     }
@@ -460,77 +453,41 @@
         Bundle optionsBundle = options1.toBundle();
         Bundle extrasBundle = new Bundle(1);
         extrasBundle.putParcelable(KEY_EXTRA_WIDGET_INTENT, widgetIntent);
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback, "LaunchSplitPair");
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_TASK_TASK ->
-                        mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
-                                null /* options2 */, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+        final RemoteTransition remoteTransition = getRemoteTransition(firstTaskId,
+                secondTaskId, callback, "LaunchSplitPair");
+        switch (launchData.getSplitLaunchType()) {
+            case SPLIT_TASK_TASK ->
+                    mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
+                            null /* options2 */, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_TASK_PENDINGINTENT ->
-                        mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle,
-                                firstTaskId, extrasBundle, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+            case SPLIT_TASK_PENDINGINTENT ->
+                    mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle,
+                            firstTaskId, extrasBundle, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_TASK_SHORTCUT ->
-                        mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle,
-                                firstTaskId, null /*options2*/, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+            case SPLIT_TASK_SHORTCUT ->
+                    mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle,
+                            firstTaskId, null /*options2*/, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_PENDINGINTENT_TASK ->
-                        mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle,
-                                secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
+            case SPLIT_PENDINGINTENT_TASK ->
+                    mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle,
+                            secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
 
-                case SPLIT_PENDINGINTENT_PENDINGINTENT ->
-                        mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut,
-                                optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle,
-                                initialStagePosition, snapPosition, remoteTransition,
-                                shellInstanceId);
+            case SPLIT_PENDINGINTENT_PENDINGINTENT ->
+                    mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut,
+                            optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle,
+                            initialStagePosition, snapPosition, remoteTransition,
+                            shellInstanceId);
 
-                case SPLIT_SHORTCUT_TASK ->
-                        mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle,
-                                secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
-                                remoteTransition, shellInstanceId);
-            }
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, secondTaskId,
-                    callback);
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_TASK_TASK ->
-                        mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle,
-                                secondTaskId, null /* options2 */, initialStagePosition,
-                                snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_TASK_PENDINGINTENT ->
-                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(secondPI,
-                                secondUserId, optionsBundle, firstTaskId, null /*options2*/,
-                                initialStagePosition, snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_TASK_SHORTCUT ->
-                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(secondShortcut,
-                                optionsBundle, firstTaskId, null /*options2*/, initialStagePosition,
-                                snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_PENDINGINTENT_TASK ->
-                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId,
-                                optionsBundle, secondTaskId, null /*options2*/,
-                                initialStagePosition, snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_PENDINGINTENT_PENDINGINTENT ->
-                        mSystemUiProxy.startIntentsWithLegacyTransition(firstPI, firstUserId,
-                                firstShortcut, optionsBundle, secondPI, secondUserId,
-                                secondShortcut, null /*options2*/, initialStagePosition,
-                                snapPosition, adapter, shellInstanceId);
-
-                case SPLIT_SHORTCUT_TASK ->
-                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(firstShortcut,
-                                optionsBundle, secondTaskId, null /*options2*/,
-                                initialStagePosition, snapPosition, adapter, shellInstanceId);
-            }
+            case SPLIT_SHORTCUT_TASK ->
+                    mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle,
+                            secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
+                            remoteTransition, shellInstanceId);
         }
+
     }
 
     /**
@@ -576,20 +533,13 @@
         }
         Bundle optionsBundle = options1.toBundle();
 
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            final RemoteTransition transition = remoteTransition == null
-                    ? getShellRemoteTransition(
-                            firstTaskId, secondTaskId, callback, "LaunchExistingPair")
-                    : remoteTransition;
-            mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
-                    stagePosition, snapPosition, transition, null /*shellInstanceId*/);
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId,
-                    secondTaskId, callback);
-            mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, secondTaskId,
-                    null /* options2 */, stagePosition, snapPosition, adapter,
-                    null /*shellInstanceId*/);
-        }
+        final RemoteTransition transition = remoteTransition == null
+                ? getRemoteTransition(
+                firstTaskId, secondTaskId, callback, "LaunchExistingPair")
+                : remoteTransition;
+        mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
+                stagePosition, snapPosition, transition, null /*shellInstanceId*/);
+
     }
 
     /**
@@ -615,34 +565,16 @@
                 ActivityThread.currentActivityThread().getApplicationThread(),
                 "LaunchAppFullscreen");
         InstanceId instanceId = mSessionInstanceIds.first;
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
-                        optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
-                        SNAP_TO_50_50, remoteTransition, instanceId);
-                case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
-                        firstUserId, optionsBundle, secondTaskId, null /*options2*/,
-                        initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
-                case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
-                        initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
-                        initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
-            }
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId,
-                    secondTaskId, callback);
-            switch (launchData.getSplitLaunchType()) {
-                case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasksWithLegacyTransition(
-                        firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
-                        initialStagePosition, SNAP_TO_50_50, adapter, instanceId);
-                case SPLIT_SINGLE_INTENT_FULLSCREEN ->
-                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId,
-                                optionsBundle, secondTaskId, null /*options2*/,
-                                initialStagePosition, SNAP_TO_50_50, adapter, instanceId);
-                case SPLIT_SINGLE_SHORTCUT_FULLSCREEN ->
-                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(
-                                initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
-                                initialStagePosition, SNAP_TO_50_50, adapter, instanceId);
-            }
+        switch (launchData.getSplitLaunchType()) {
+            case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
+                    optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
+                    SNAP_TO_50_50, remoteTransition, instanceId);
+            case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
+                    firstUserId, optionsBundle, secondTaskId, null /*options2*/,
+                    initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
+            case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
+                    initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
+                    initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
         }
     }
 
@@ -660,7 +592,7 @@
         mSplitFromDesktopController = controller;
     }
 
-    private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
+    private RemoteTransition getRemoteTransition(int firstTaskId, int secondTaskId,
             @Nullable Consumer<Boolean> callback, String transitionName) {
         final RemoteSplitLaunchTransitionRunner animationRunner =
                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
@@ -668,14 +600,6 @@
                 ActivityThread.currentActivityThread().getApplicationThread(), transitionName);
     }
 
-    private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
-            @Nullable Consumer<Boolean> callback) {
-        final RemoteSplitLaunchAnimationRunner animationRunner =
-                new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback);
-        return new RemoteAnimationAdapter(animationRunner, 300, 150,
-                ActivityThread.currentActivityThread().getApplicationThread());
-    }
-
     /**
      * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from
      * {@link #mSplitSelectDataHolder}
@@ -807,55 +731,6 @@
     }
 
     /**
-     * LEGACY
-     * Remote animation runner for animation to launch an app.
-     */
-    private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat {
-
-        private final int mInitialTaskId;
-        private final int mSecondTaskId;
-        private final Consumer<Boolean> mSuccessCallback;
-
-        RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
-                @Nullable Consumer<Boolean> successCallback) {
-            mInitialTaskId = initialTaskId;
-            mSecondTaskId = secondTaskId;
-            mSuccessCallback = successCallback;
-        }
-
-        @Override
-        public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
-                RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
-                Runnable finishedCallback) {
-            postAsyncCallback(mHandler,
-                    () -> mSplitAnimationController
-                            .playSplitLaunchAnimation(mLaunchingTaskView,
-                            mLaunchingIconView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
-                            nonApps, mStateManager, mDepthController, null /* info */, null /* t */,
-                            () -> {
-                                finishedCallback.run();
-                                if (mSuccessCallback != null) {
-                                    mSuccessCallback.accept(true);
-                                }
-                                resetState();
-                            },
-                            QuickStepContract.getWindowCornerRadius(mContainer.asContext())));
-        }
-
-        @Override
-        public void onAnimationCancelled() {
-            postAsyncCallback(mHandler, () -> {
-                if (mSuccessCallback != null) {
-                    // Launching legacy tasks while recents animation is running will always cause
-                    // onAnimationCancelled to be called (should be fixed w/ shell transitions?)
-                    mSuccessCallback.accept(mRecentsAnimationRunning);
-                }
-                resetState();
-            });
-        }
-    }
-
-    /**
      * To be called whenever we exit split selection state. If
      * {@link FeatureFlags#enableSplitContextually()} is set, this should be the
      * central way split is getting reset, which should then go through the callbacks to reset
@@ -961,7 +836,7 @@
         private final int mSplitPlaceholderSize;
         private final int mSplitPlaceholderInset;
         private ActivityManager.RunningTaskInfo mTaskInfo;
-        private ISplitSelectListener mSplitSelectListener;
+        private DesktopSplitSelectListenerImpl mSplitSelectListener;
         private Drawable mAppIcon;
 
         public SplitFromDesktopController(QuickstepLauncher launcher,
@@ -972,21 +847,14 @@
                     R.dimen.split_placeholder_size);
             mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
                     R.dimen.split_placeholder_inset);
-            mSplitSelectListener = new ISplitSelectListener.Stub() {
-                @Override
-                public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
-                        int splitPosition, Rect taskBounds) {
-                    MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition,
-                            taskBounds));
-                    return true;
-                }
-            };
+            mSplitSelectListener = new DesktopSplitSelectListenerImpl(this);
             SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener);
         }
 
         void onDestroy() {
             SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener(
                     mSplitSelectListener);
+            mSplitSelectListener.release();
             mSplitSelectListener = null;
         }
 
@@ -1079,4 +947,35 @@
             }
         }
     }
+
+    /**
+     * Wrapper for the ISplitSelectListener stub to prevent lingering references to the launcher
+     * activity via the controller.
+     */
+    private static class DesktopSplitSelectListenerImpl extends ISplitSelectListener.Stub {
+
+        private SplitFromDesktopController mController;
+
+        DesktopSplitSelectListenerImpl(@NonNull SplitFromDesktopController controller) {
+            mController = controller;
+        }
+
+        /**
+         * Clears any references to the controller.
+         */
+        void release() {
+            mController = null;
+        }
+
+        @Override
+        public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+                int splitPosition, Rect taskBounds) {
+            MAIN_EXECUTOR.execute(() -> {
+                if (mController != null) {
+                    mController.enterSplitSelect(taskInfo, splitPosition, taskBounds);
+                }
+            });
+            return true;
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 9ce2277..384945b 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -30,6 +30,8 @@
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
+import com.android.launcher3.testing.TestLogging
+import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TransformingTouchDelegate
@@ -213,7 +215,15 @@
     }
 
     override fun needsUpdate(dataChange: Int, flag: Int) =
-        if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false
+        if (flag == FLAG_UPDATE_CORNER_RADIUS) false else super.needsUpdate(dataChange, flag)
+
+    override fun onIconLoaded(taskContainer: TaskContainer) {
+        // Update contentDescription of snapshotView only, individual task icon is unused.
+        taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
+    }
+
+    // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
+    override fun onIconUnloaded(taskContainer: TaskContainer) {}
 
     // thumbnailView is laid out differently and is handled in onMeasure
     override fun updateThumbnailSize() {}
@@ -228,6 +238,11 @@
 
     override fun launchTaskAnimated(): RunnableList? {
         val recentsView = recentsView ?: return null
+        TestLogging.recordEvent(
+            TestProtocol.SEQUENCE_MAIN,
+            "launchDesktopFromRecents",
+            taskIds.contentToString()
+        )
         val endCallback = RunnableList()
         val desktopController = recentsView.desktopRecentsController
         checkNotNull(desktopController) { "recentsController is null" }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8b6bc39..5a6c278 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -135,6 +135,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.PagedView;
@@ -553,6 +554,7 @@
     // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
     private float mGridProgress = 0;
     private float mTaskThumbnailSplashAlpha = 0;
+    private boolean mBorderEnabled = false;
     private boolean mShowAsGridLastOnLayout = false;
     private final IntSet mTopRowIdSet = new IntSet();
     private int mClearAllShortTotalWidthTranslation = 0;
@@ -1047,8 +1049,11 @@
     @Override
     @Nullable
     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
+        if (enableRefactorTaskThumbnail()) {
+            mHelper.onTaskThumbnailChanged(taskId, thumbnailData);
+            return null;
+        }
         if (mHandleTaskStackChanges) {
-            // TODO(b/342560598): Handle onTaskThumbnailChanged for new TTV.
             if (!enableRefactorTaskThumbnail()) {
                 TaskView taskView = getTaskViewByTaskId(taskId);
                 if (taskView != null) {
@@ -1067,6 +1072,7 @@
 
     @Override
     public void onTaskIconChanged(String pkg, UserHandle user) {
+        // TODO(b/342560598): Listen in TaskRepository and reload.
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView tv = requireTaskViewAt(i);
             Task task = tv.getFirstTask();
@@ -1082,47 +1088,36 @@
 
     @Override
     public void onTaskIconChanged(int taskId) {
+        if (enableRefactorTaskThumbnail()) {
+            return;
+        }
         TaskView taskView = getTaskViewByTaskId(taskId);
         if (taskView != null) {
             taskView.refreshTaskThumbnailSplash();
         }
     }
 
-    /**
-     * Update the thumbnail(s) of the relevant TaskView.
-     *
-     * @param refreshNow Refresh immediately if it's true.
-     */
-    @Nullable
-    public TaskView updateThumbnail(
-            HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow) {
-        if (enableRefactorTaskThumbnail()) {
-            // TODO(b/342560598): Handle updateThumbnail for new TTV.
-            return null;
-        }
-        TaskView updatedTaskView = null;
-        for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
-            Integer id = entry.getKey();
-            ThumbnailData thumbnail = entry.getValue();
-            TaskView taskView = getTaskViewByTaskId(id);
-            if (taskView == null) {
-                continue;
+    /** Updates the thumbnail(s) of the relevant TaskView. */
+    public void updateThumbnail(Map<Integer, ThumbnailData> thumbnailData) {
+        if (!enableRefactorTaskThumbnail()) {
+            for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
+                Integer id = entry.getKey();
+                ThumbnailData thumbnail = entry.getValue();
+                TaskView taskView = getTaskViewByTaskId(id);
+                if (taskView == null) {
+                    continue;
+                }
+                // taskView could be a GroupedTaskView, so select the relevant task by ID
+                TaskContainer taskContainer = taskView.getTaskContainerById(id);
+                if (taskContainer == null) {
+                    continue;
+                }
+                Task task = taskContainer.getTask();
+                TaskThumbnailViewDeprecated taskThumbnailViewDeprecated =
+                        taskContainer.getThumbnailViewDeprecated();
+                taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, /*refreshNow=*/false);
             }
-            // taskView could be a GroupedTaskView, so select the relevant task by ID
-            TaskContainer taskAttributes = taskView.getTaskContainerById(id);
-            if (taskAttributes == null) {
-                continue;
-            }
-            Task task = taskAttributes.getTask();
-            TaskThumbnailViewDeprecated taskThumbnailViewDeprecated =
-                    taskAttributes.getThumbnailViewDeprecated();
-            taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, refreshNow);
-            // thumbnailData can contain 1-2 ids, but they should correspond to the same
-            // TaskView, so overwriting is ok
-            updatedTaskView = taskView;
         }
-
-        return updatedTaskView;
     }
 
     @Override
@@ -1532,6 +1527,7 @@
      * Enable or disable showing border on hover and focus change on task views
      */
     public void setTaskBorderEnabled(boolean enabled) {
+        mBorderEnabled = enabled;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = requireTaskViewAt(i);
@@ -2051,6 +2047,7 @@
                 taskView.setFullscreenProgress(mFullscreenProgress);
                 taskView.setModalness(mTaskModalness);
                 taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha);
+                taskView.setBorderEnabled(mBorderEnabled);
             }
         }
         // resetTaskVisuals is called at the end of dismiss animation which could update
@@ -2440,10 +2437,6 @@
                 List<Task> tasksToUpdate = containers.stream()
                         .map(TaskContainer::getTask)
                         .collect(Collectors.toCollection(ArrayList::new));
-                if (enableRefactorTaskThumbnail()) {
-                    visibleTaskIds.addAll(
-                            tasksToUpdate.stream().map((task) -> task.key.id).toList());
-                }
                 if (mTmpRunningTasks != null) {
                     for (Task t : mTmpRunningTasks) {
                         // Skip loading if this is the task that we are animating into
@@ -2451,6 +2444,10 @@
                         tasksToUpdate.removeIf(task -> task == t);
                     }
                 }
+                if (enableRefactorTaskThumbnail()) {
+                    visibleTaskIds.addAll(
+                            tasksToUpdate.stream().map((task) -> task.key.id).toList());
+                }
                 if (tasksToUpdate.isEmpty()) {
                     continue;
                 }
@@ -2507,6 +2504,11 @@
             mModel.preloadCacheIfNeeded();
         }
 
+        if (enableRefactorTaskThumbnail()) {
+            // TODO(b/342560598): Listen in TaskRepository and reload.
+            return;
+        }
+
         // Whenever the high res loading state changes, poke each of the visible tasks to see if
         // they want to updated their thumbnail state
         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
@@ -4919,6 +4921,9 @@
                 mSplitHiddenTaskView.updateSnapshotRadius();
             });
         } else if (isInitiatingSplitFromTaskView) {
+            if (Flags.enableHoverOfChildElementsInTaskview()) {
+                mSplitHiddenTaskView.setBorderEnabled(false);
+            }
             // Splitting from Overview for fullscreen task
             createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration,
                     true /* dismissingForSplitSelection*/);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewHelper.kt
index 05c2462..e8c9718 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewHelper.kt
@@ -60,9 +60,6 @@
         // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside
         // viewAttachedScope.
         recentsViewModel.setRunningTaskShowScreenshot(true)
-        if (updatedThumbnails != null) {
-            recentsViewModel.setThumbnailOverride(updatedThumbnails)
-        }
         viewAttachedScope.launch {
             recentsViewModel.waitForRunningTaskShowScreenshotToUpdate()
             if (updatedThumbnails != null) {
@@ -71,4 +68,8 @@
             ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
         }
     }
+
+    fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData) {
+        recentsViewModel.addOrUpdateThumbnailOverride(mapOf(taskId to thumbnailData))
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index e10d38c..158ae33 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -42,7 +42,11 @@
     companion object {
         const val TAG = "TaskMenuViewWithArrow"
 
-        fun showForTask(taskContainer: TaskContainer, alignedOptionIndex: Int = 0): Boolean {
+        fun showForTask(
+            taskContainer: TaskContainer,
+            alignedOptionIndex: Int = 0,
+            onClosedCallback: Runnable? = null
+        ): Boolean {
             val container: RecentsViewContainer =
                 RecentsViewContainer.containerFromContext(taskContainer.taskView.context)
             val taskMenuViewWithArrow =
@@ -52,7 +56,11 @@
                     false
                 ) as TaskMenuViewWithArrow<*>
 
-            return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex)
+            return taskMenuViewWithArrow.populateAndShowForTask(
+                taskContainer,
+                alignedOptionIndex,
+                onClosedCallback
+            )
         }
     }
 
@@ -98,6 +106,7 @@
     private var iconView: IconView? = null
     private var scrim: View? = null
     private val scrimAlpha = 0.8f
+    private var onClosedCallback: Runnable? = null
 
     override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0
 
@@ -141,7 +150,8 @@
 
     private fun populateAndShowForTask(
         taskContainer: TaskContainer,
-        alignedOptionIndex: Int
+        alignedOptionIndex: Int,
+        onClosedCallback: Runnable?
     ): Boolean {
         if (isAttachedToWindow) {
             return false
@@ -150,6 +160,7 @@
         taskView = taskContainer.taskView
         this.taskContainer = taskContainer
         this.alignedOptionIndex = alignedOptionIndex
+        this.onClosedCallback = onClosedCallback
         if (!populateMenu()) return false
         addScrim()
         show()
@@ -252,6 +263,7 @@
         super.closeComplete()
         popupContainer.removeView(scrim)
         popupContainer.removeView(iconView)
+        onClosedCallback?.run()
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 292595c..176a538 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -48,6 +48,7 @@
 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.enableOverviewIconMenu
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
@@ -413,6 +414,26 @@
             focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true)
         }
 
+    /**
+     * Used to cache the hover border state so we don't repeatedly call the border animator with
+     * every hover event when the user hasn't crossed the threshold of the [thumbnailBounds].
+     */
+    private var hoverBorderVisible = false
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            Log.d(
+                TAG,
+                "${taskIds.contentToString()} - setting border animator visibility to: $field"
+            )
+            hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true)
+        }
+
+    // Used to cache thumbnail bounds to avoid recalculating on every hover move.
+    private var thumbnailBounds = Rect()
+
     private var focusTransitionProgress = 1f
         set(value) {
             field = value
@@ -511,20 +532,28 @@
     override fun onHoverEvent(event: MotionEvent): Boolean {
         if (borderEnabled) {
             when (event.action) {
-                MotionEvent.ACTION_HOVER_ENTER ->
-                    hoverBorderAnimator?.setBorderVisibility(visible = true, animated = true)
-                MotionEvent.ACTION_HOVER_EXIT ->
-                    hoverBorderAnimator?.setBorderVisibility(visible = false, animated = true)
+                MotionEvent.ACTION_HOVER_ENTER -> {
+                    hoverBorderVisible =
+                        if (enableHoverOfChildElementsInTaskview()) {
+                            getThumbnailBounds(thumbnailBounds)
+                            event.isWithinThumbnailBounds()
+                        } else {
+                            true
+                        }
+                }
+                MotionEvent.ACTION_HOVER_MOVE ->
+                    if (enableHoverOfChildElementsInTaskview())
+                        hoverBorderVisible = event.isWithinThumbnailBounds()
+                MotionEvent.ACTION_HOVER_EXIT -> hoverBorderVisible = false
                 else -> {}
             }
         }
         return super.onHoverEvent(event)
     }
 
-    // avoid triggering hover event on child elements which would cause HOVER_EXIT for this
-    // task view
-    override fun onInterceptHoverEvent(event: MotionEvent) =
-        if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event)
+    override fun onInterceptHoverEvent(event: MotionEvent): Boolean =
+        if (enableHoverOfChildElementsInTaskview()) super.onInterceptHoverEvent(event)
+        else if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event)
 
     override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
         val recentsView = recentsView ?: return false
@@ -567,6 +596,9 @@
                 it.right = width
                 it.bottom = height
             }
+        if (enableHoverOfChildElementsInTaskview()) {
+            getThumbnailBounds(thumbnailBounds)
+        }
     }
 
     override fun onRecycle() {
@@ -579,6 +611,7 @@
         setOverlayEnabled(false)
         onTaskListVisibilityChanged(false)
         borderEnabled = false
+        hoverBorderVisible = false
         taskViewId = UNBOUND_TASK_VIEW_ID
         taskContainers.forEach { it.destroy() }
     }
@@ -868,18 +901,11 @@
                             it.task.icon = icon
                             it.task.titleDescription = contentDescription
                             it.task.title = title
-                            setIcon(it.iconView, icon)
-                            if (enableOverviewIconMenu()) {
-                                setText(it.iconView, title)
-                            }
-                            it.digitalWellBeingToast?.initialize(it.task)
+                            onIconLoaded(it)
                         }
                         ?.also { request -> pendingIconLoadRequests.add(request) }
                 } else {
-                    setIcon(it.iconView, null)
-                    if (enableOverviewIconMenu()) {
-                        setText(it.iconView, null)
-                    }
+                    onIconUnloaded(it)
                 }
             }
         }
@@ -898,6 +924,21 @@
         pendingIconLoadRequests.clear()
     }
 
+    protected open fun onIconLoaded(taskContainer: TaskContainer) {
+        setIcon(taskContainer.iconView, taskContainer.task.icon)
+        if (enableOverviewIconMenu()) {
+            setText(taskContainer.iconView, taskContainer.task.title)
+        }
+        taskContainer.digitalWellBeingToast?.initialize(taskContainer.task)
+    }
+
+    protected open fun onIconUnloaded(taskContainer: TaskContainer) {
+        setIcon(taskContainer.iconView, null)
+        if (enableOverviewIconMenu()) {
+            setText(taskContainer.iconView, null)
+        }
+    }
+
     protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) {
         with(iconView) {
             if (icon != null) {
@@ -1119,6 +1160,11 @@
             isClickableAsLiveTile = true
             return runnableList
         }
+        TestLogging.recordEvent(
+            TestProtocol.SEQUENCE_MAIN,
+            "composeRecentsLaunchAnimator",
+            taskIds.contentToString()
+        )
         val runnableList = RunnableList()
         with(AnimatorSet()) {
             TaskViewUtils.composeRecentsLaunchAnimator(
@@ -1225,10 +1271,17 @@
 
     private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean {
         val recentsView = recentsView ?: return false
+        if (enableHoverOfChildElementsInTaskview()) {
+            // Disable hover on all TaskView's whilst menu is showing.
+            recentsView.setTaskBorderEnabled(false)
+        }
         return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) {
             menuContainer.iconView.revealAnim(/* isRevealing= */ true)
             TaskMenuView.showForTask(menuContainer) {
                 menuContainer.iconView.revealAnim(/* isRevealing= */ false)
+                if (enableHoverOfChildElementsInTaskview()) {
+                    recentsView.setTaskBorderEnabled(true)
+                }
             }
         } else if (container.deviceProfile.isTablet) {
             val alignedOptionIndex =
@@ -1248,9 +1301,17 @@
                 } else {
                     0
                 }
-            TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex)
+            TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) {
+                if (enableHoverOfChildElementsInTaskview()) {
+                    recentsView.setTaskBorderEnabled(true)
+                }
+            }
         } else {
-            TaskMenuView.showForTask(menuContainer)
+            TaskMenuView.showForTask(menuContainer) {
+                if (enableHoverOfChildElementsInTaskview()) {
+                    recentsView.setTaskBorderEnabled(true)
+                }
+            }
         }
     }
 
@@ -1398,7 +1459,6 @@
 
     protected open fun refreshTaskThumbnailSplash() {
         if (!enableRefactorTaskThumbnail()) {
-            // TODO(b/342560598) handle onTaskIconChanged
             taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() }
         }
     }
@@ -1584,6 +1644,10 @@
         override fun close() {}
     }
 
+    private fun MotionEvent.isWithinThumbnailBounds(): Boolean {
+        return thumbnailBounds.contains(x.toInt(), y.toInt())
+    }
+
     companion object {
         private const val TAG = "TaskView"
         const val FLAG_UPDATE_ICON = 1
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
new file mode 100644
index 0000000..3e0d6b5
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar.bubbles
+
+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.common.bubbles.BubbleInfo
+import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.ViewScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** Screenshot tests for [BubbleView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() =
+            DeviceEmulationSpec.forDisplays(
+                Displays.Phone,
+                isDarkTheme = false,
+                isLandscape = false
+            )
+    }
+
+    @get:Rule
+    val screenshotRule =
+        ViewScreenshotTestRule(
+            emulationSpec,
+            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec))
+        )
+
+    @Test
+    fun bubbleView_hasUnseenContent() {
+        screenshotRule.screenshotTest("bubbleView_hasUnseenContent") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleView()
+        }
+    }
+
+    @Test
+    fun bubbleView_seen() {
+        screenshotRule.screenshotTest("bubbleView_seen") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleView().apply { markSeen() }
+        }
+    }
+
+    @Test
+    fun bubbleView_badgeHidden() {
+        screenshotRule.screenshotTest("bubbleView_badgeHidden") { activity ->
+            activity.actionBar?.hide()
+            setupBubbleView().apply { setBadgeScale(0f) }
+        }
+    }
+
+    private fun setupBubbleView(): 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 bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false)
+        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 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/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
index e583f63..961d4dc 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
@@ -14,11 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.test
 
+import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.Utilities
+import com.android.launcher3.taskbar.TOOLTIP_STEP_FEATURES
+import com.android.launcher3.taskbar.TOOLTIP_STEP_NONE
+import com.android.launcher3.taskbar.TOOLTIP_STEP_PINNING
+import com.android.launcher3.taskbar.TOOLTIP_STEP_SWIPE
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarEduTooltipController
 import com.android.launcher3.taskbar.rules.TaskbarModeRule
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
@@ -35,12 +42,14 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(LauncherMultivalentJUnit::class)
 @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+@Ignore
 class TaskbarEduTooltipControllerTest {
 
     private val context =
@@ -48,25 +57,25 @@
             InstrumentationRegistry.getInstrumentation().targetContext
         )
 
-    @get:Rule
+    @get:Rule(order = 0)
     val tooltipStepPreferenceRule =
         TaskbarPreferenceRule(
             context,
             OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem,
         )
 
-    @get:Rule
+    @get:Rule(order = 1)
     val searchEduPreferenceRule =
         TaskbarPreferenceRule(
             context,
             OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN,
         )
 
-    @get:Rule val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context)
+    @get:Rule(order = 2) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context)
 
-    @get:Rule val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
 
-    @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+    @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
 
     @InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController
 
@@ -77,6 +86,7 @@
 
     @Before
     fun setUp() {
+        Log.e("Taskbar", "TaskbarEduTooltipControllerTest test started")
         Utilities.disableRunningInTestHarnessForTests()
     }
 
@@ -85,6 +95,7 @@
         if (wasInTestHarness) {
             Utilities.enableRunningInTestHarnessForTests()
         }
+        Log.e("Taskbar", "TaskbarEduTooltipControllerTest test completed")
     }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index e9c0dd6..21eb3e0 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -35,8 +35,8 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
 import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
-import com.android.launcher3.taskbar.bubbles.BubbleStashController
 import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
 import com.android.wm.shell.common.bubbles.BubbleInfo
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
@@ -78,13 +78,13 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // let the animation start and wait for it to complete
@@ -122,13 +122,13 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // let the animation start and wait for it to complete
@@ -165,13 +165,13 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // wait for the animation to start
@@ -205,13 +205,13 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // let the animation start and wait for it to complete
@@ -246,13 +246,13 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // wait for the animation to start
@@ -270,6 +270,123 @@
     }
 
     @Test
+    fun animateBubbleInForStashed_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = true)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.isExpanded).isTrue()
+
+        // verify there is no hide animation
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_expandedWhileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        handleAnimator.assertIsRunning()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the animation finish
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_expandedWhileFullyIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // wait for the animation to end
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+    }
+
+    @Test
     fun animateToInitialState_inApp() {
         setUpBubbleBar()
         setUpBubbleStashController()
@@ -278,7 +395,7 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
@@ -321,7 +438,7 @@
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
-        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
@@ -336,17 +453,11 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         barAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
-        assertThat(bubbleBarView.alpha).isEqualTo(1)
-        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-
-        assertThat(animatorScheduler.delayedBlock).isNotNull()
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
-
         assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
 
+        assertThat(animatorScheduler.delayedBlock).isNull()
         verify(bubbleStashController).showBubbleBarImmediate()
     }
 
@@ -385,6 +496,79 @@
     }
 
     @Test
+    fun animateToInitialState_expandedWhileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+        }
+
+        val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true }
+
+        bubbleBarAnimator.assertIsRunning()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the animation finish
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateToInitialState_expandedWhileFullyIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+    }
+
+    @Test
     fun animateBubbleBarForCollapsed() {
         setUpBubbleBar()
         setUpBubbleStashController()
@@ -397,7 +581,7 @@
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleBarForCollapsed(bubble)
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
         }
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
@@ -424,6 +608,142 @@
         verify(bubbleStashController).showBubbleBarImmediate()
     }
 
+    @Test
+    fun animateBubbleBarForCollapsed_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = true)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there is no hide animation
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true }
+
+        // verify there is a pending hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the animation finish
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateBubbleBarForCollapsed_expandingWhileFullyIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there is a pending hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -451,14 +771,22 @@
     private fun setUpBubbleStashController() {
         bubbleStashController = mock<BubbleStashController>()
         whenever(bubbleStashController.isStashed).thenReturn(true)
-        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
+        whenever(bubbleStashController.getDiffBetweenHandleAndBarCenters())
             .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
-        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
+        whenever(bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation())
             .thenReturn(HANDLE_TRANSLATION)
         whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
             .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
     }
 
+    private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) {
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(ty)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+    }
+
     private fun <T> PhysicsAnimator<T>.assertIsRunning() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             assertThat(isRunning()).isTrue()
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
index 00ad3b7..0f8a2c3 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
@@ -17,7 +17,7 @@
 package com.android.launcher3.taskbar.bubbles.stashing
 
 class ImmediateAction : BubbleStashController.ControllersAfterInitAction {
-    override fun runAfterInit(action: () -> Unit) = action.invoke()
+    override fun runAfterInit(action: Runnable) = action.run()
 }
 
 class DefaultDimensionsProvider(
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
index f75e542..f7e4576 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
@@ -24,6 +24,7 @@
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 import com.android.launcher3.util.NavigationMode
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -31,6 +32,7 @@
 import org.junit.runner.RunWith
 
 @RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarModeRuleTest {
 
     private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
index a709133..a515405 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 import com.android.launcher3.util.window.WindowManagerProxy
 import com.google.android.apps.nexuslauncher.deviceemulator.TestWindowManagerProxy
 import com.google.common.truth.Truth.assertThat
@@ -28,6 +29,7 @@
 import org.junit.runners.model.Statement
 
 @RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarPinningPreferenceRuleTest {
     private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
index 22d2079..46817d2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
 import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.Description
@@ -26,6 +27,7 @@
 import org.junit.runners.model.Statement
 
 @RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarPreferenceRuleTest {
 
     private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
index 234e499..5d4fdc5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -34,7 +34,7 @@
 import org.junit.runners.model.Statement
 
 @RunWith(LauncherMultivalentJUnit::class)
-@EmulatedDevices(["pixelFoldable2023"])
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarUnitTestRuleTest {
 
     private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
index ad4b4de..4834d48 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
@@ -25,6 +25,7 @@
 import org.junit.runner.RunWith
 
 @RunWith(LauncherMultivalentJUnit::class)
+@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarWindowSandboxContextTest {
 
     private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
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 ea2e484..d2479bc 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -20,8 +20,9 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.ALLOW_ROTATION
 import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
-import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
+import com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY
 import com.android.launcher3.logging.InstanceId
 import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED
@@ -32,6 +33,10 @@
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED
+import com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY
+import com.google.android.apps.nexuslauncher.PrefKey.KEY_ENABLE_MINUS_ONE
+import com.google.android.apps.nexuslauncher.PrefKey.OVERVIEW_SUGGESTED_ACTIONS
+import com.google.android.apps.nexuslauncher.PrefKey.SMARTSPACE_ON_HOME_SCREEN
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -62,6 +67,7 @@
     @Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
 
     private var mDefaultThemedIcons = false
+    private var mDefaultAllowRotation = false
 
     @Before
     fun setUp() {
@@ -70,8 +76,11 @@
         whenever(mStatsLogManager.logger()).doReturn(mMockLogger)
         whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger)
         mDefaultThemedIcons = LauncherPrefs.get(mContext).get(THEMED_ICONS)
+        mDefaultAllowRotation = LauncherPrefs.get(mContext).get(ALLOW_ROTATION)
         // To match the default value of THEMED_ICONS
         LauncherPrefs.get(mContext).put(THEMED_ICONS, false)
+        // To match the default value of ALLOW_ROTATION
+        LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false)
 
         mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
     }
@@ -79,18 +88,19 @@
     @After
     fun tearDown() {
         LauncherPrefs.get(mContext).put(THEMED_ICONS, mDefaultThemedIcons)
-        mSystemUnderTest.close()
+        LauncherPrefs.get(mContext).put(ALLOW_ROTATION, mDefaultAllowRotation)
     }
 
     @Test
     fun loggingPrefs_correctDefaultValue() {
-        assertThat(mSystemUnderTest.loggingPrefs["pref_allowRotation"]!!.defaultValue).isFalse()
-        assertThat(mSystemUnderTest.loggingPrefs["pref_add_icon_to_home"]!!.defaultValue).isTrue()
-        assertThat(mSystemUnderTest.loggingPrefs["pref_overview_action_suggestions"]!!.defaultValue)
-            .isTrue()
-        assertThat(mSystemUnderTest.loggingPrefs["pref_smartspace_home_screen"]!!.defaultValue)
-            .isTrue()
-        assertThat(mSystemUnderTest.loggingPrefs["pref_enable_minus_one"]!!.defaultValue).isTrue()
+        val systemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+
+        assertThat(systemUnderTest.loggingPrefs[ALLOW_ROTATION_PREFERENCE_KEY]!!.defaultValue)
+            .isFalse()
+        assertThat(systemUnderTest.loggingPrefs[ADD_ICON_PREFERENCE_KEY]!!.defaultValue).isTrue()
+        assertThat(systemUnderTest.loggingPrefs[OVERVIEW_SUGGESTED_ACTIONS]!!.defaultValue).isTrue()
+        assertThat(systemUnderTest.loggingPrefs[SMARTSPACE_ON_HOME_SCREEN]!!.defaultValue).isTrue()
+        assertThat(systemUnderTest.loggingPrefs[KEY_ENABLE_MINUS_ONE]!!.defaultValue).isTrue()
     }
 
     @Test
@@ -101,24 +111,16 @@
         val capturedEvents = mEventCaptor.allValues
         assertThat(capturedEvents.isNotEmpty()).isTrue()
         verifyDefaultEvent(capturedEvents)
-        // pref_allowRotation false
         assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_DISABLED.id })
             .isTrue()
     }
 
     @Test
-    fun logSnapshot_updateValue() {
-        LauncherPrefs.get(mContext)
-            .put(
-                item =
-                    backedUpItem(
-                        sharedPrefKey = "pref_allowRotation",
-                        defaultValue = false,
-                    ),
-                value = true
-            )
+    fun logSnapshot_updateAllowRotation() {
+        LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = true)
 
-        mSystemUnderTest.logSnapshot(mInstanceId)
+        // This a new object so the values of mLoggablePrefs will be different
+        SettingsChangeLogger(mContext, mStatsLogManager).logSnapshot(mInstanceId)
 
         verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
         val capturedEvents = mEventCaptor.allValues
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index e488413..d94a351 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -65,7 +65,7 @@
         setThumbnailOverrideInternal(thumbnailOverrideMap)
     }
 
-    override fun setThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
+    override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
         setThumbnailOverrideInternal(thumbnailOverride)
     }
 
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 aee5d1e..b34e156 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
@@ -193,57 +193,79 @@
     }
 
     @Test
-    fun setThumbnailOverrideOverrideThumbnails() = runTest {
+    fun addThumbnailOverrideOverrideThumbnails() = runTest {
         recentsModel.seedTasks(defaultTaskList)
         val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-        val thumbnailOverride = createThumbnailData()
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        systemUnderTest.setVisibleTasks(listOf(1))
-        systemUnderTest.setThumbnailOverride(mapOf(2 to thumbnailOverride))
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // .drop(1) to ignore initial null content before from thumbnail was loaded.
-        assertThat(systemUnderTest.getThumbnailById(1).drop(1).first()!!.thumbnail)
-            .isEqualTo(bitmap1)
-        assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2)
-    }
-
-    @Test
-    fun setThumbnailOverrideClearedWhenTaskBecomeInvisible() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-        val thumbnailOverride = createThumbnailData()
+        val thumbnailOverride2 = createThumbnailData()
         systemUnderTest.getAllTaskData(forceRefresh = true)
 
         systemUnderTest.setVisibleTasks(listOf(1, 2))
-        systemUnderTest.setThumbnailOverride(mapOf(2 to thumbnailOverride))
-        systemUnderTest.setVisibleTasks(listOf(1))
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-        // .drop(1) to ignore initial null content before from thumbnail was loaded.
-        assertThat(systemUnderTest.getThumbnailById(1).drop(1).first()!!.thumbnail)
-            .isEqualTo(bitmap1)
-        assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2)
-    }
-
-    @Test
-    fun setThumbnailOverrideDoesNotOverrideInvisibleTasks() = runTest {
-        recentsModel.seedTasks(defaultTaskList)
-        val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-        val thumbnailOverride = createThumbnailData()
-        systemUnderTest.getAllTaskData(forceRefresh = true)
-
-        systemUnderTest.setVisibleTasks(listOf(1, 2))
-        systemUnderTest.setThumbnailOverride(mapOf(2 to thumbnailOverride))
+        systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
 
         // .drop(1) to ignore initial null content before from thumbnail was loaded.
         assertThat(systemUnderTest.getThumbnailById(1).drop(1).first()!!.thumbnail)
             .isEqualTo(bitmap1)
         assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail)
-            .isEqualTo(thumbnailOverride.thumbnail)
+            .isEqualTo(thumbnailOverride2.thumbnail)
+    }
+
+    @Test
+    fun addThumbnailOverrideMultipleOverrides() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        val thumbnailOverride1 = createThumbnailData()
+        val thumbnailOverride2 = createThumbnailData()
+        val thumbnailOverride3 = createThumbnailData()
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+        systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1))
+        systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
+        systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride3))
+
+        assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail)
+            .isEqualTo(thumbnailOverride1.thumbnail)
+        assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail)
+            .isEqualTo(thumbnailOverride3.thumbnail)
+    }
+
+    @Test
+    fun addThumbnailOverrideClearedWhenTaskBecomeInvisible() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+        val thumbnailOverride1 = createThumbnailData()
+        val thumbnailOverride2 = createThumbnailData()
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+        systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1))
+        systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
+        // Making task 2 invisible and visible again should clear the override
+        systemUnderTest.setVisibleTasks(listOf(1))
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // .drop(1) to ignore initial null content before from thumbnail was loaded.
+        assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail)
+            .isEqualTo(thumbnailOverride1.thumbnail)
+        assertThat(systemUnderTest.getThumbnailById(2).drop(1).first()!!.thumbnail)
+            .isEqualTo(bitmap2)
+    }
+
+    @Test
+    fun addThumbnailOverrideDoesNotOverrideInvisibleTasks() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
+        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+        val thumbnailOverride = createThumbnailData()
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1))
+        systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride))
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // .drop(1) to ignore initial null content before from thumbnail was loaded.
+        assertThat(systemUnderTest.getThumbnailById(1).drop(1).first()!!.thumbnail)
+            .isEqualTo(bitmap1)
+        assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2)
     }
 
     private fun createTaskWithId(taskId: Int) =
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 00dbcc1..dc16475 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
@@ -72,9 +72,8 @@
 
     @Test
     fun thumbnailOverrideWaitAndReset() = runTest {
-        val thumbnailData1 = createThumbnailData()
-        val thumbnailData2 = createThumbnailData()
-        val thumbnailDataOverride = createThumbnailData()
+        val thumbnailData1 = createThumbnailData().apply { snapshotId = 1 }
+        val thumbnailData2 = createThumbnailData().apply { snapshotId = 2 }
         tasksRepository.seedTasks(tasks)
         tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2))
 
@@ -87,15 +86,16 @@
         assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
         assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2)
 
-        val thumbnailUpdate = mapOf(2 to thumbnailDataOverride)
         systemUnderTest.setRunningTaskShowScreenshot(true)
-        systemUnderTest.setThumbnailOverride(thumbnailUpdate)
+        val thumbnailOverride = mapOf(2 to createThumbnailData().apply { snapshotId = 3 })
+        systemUnderTest.addOrUpdateThumbnailOverride(thumbnailOverride)
 
         systemUnderTest.waitForRunningTaskShowScreenshotToUpdate()
-        systemUnderTest.waitForThumbnailsToUpdate(thumbnailUpdate)
+        val expectedUpdate = mapOf(2 to createThumbnailData().apply { snapshotId = 3 })
+        systemUnderTest.waitForThumbnailsToUpdate(expectedUpdate)
 
         assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
-        assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailDataOverride)
+        assertThat(thumbnailDataFlow2.first()?.snapshotId).isEqualTo(3)
 
         systemUnderTest.onReset()
 
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index c0ff189..88ffeea 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -20,10 +20,12 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.content.res.Resources
 import android.os.Process
 import android.os.UserHandle
 import android.platform.test.rule.TestWatcher
 import android.testing.AndroidTestingRunner
+import com.android.internal.R
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
 import com.android.launcher3.model.data.AppInfo
@@ -73,6 +75,7 @@
     @Mock private lateinit var mockIconCache: TaskIconCache
     @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
@@ -88,6 +91,10 @@
         super.setup()
         userHandle = Process.myUserHandle()
 
+        // Set desktop mode supported
+        whenever(mockContext.getResources()).thenReturn(mockResources)
+        whenever(mockResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true)
+
         whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
         whenever(mockRecentsModel.unregisterRecentTasksChangedListener()).then {
             recentTasksChangedListener = null
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index c213dbb..cbc8441 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -30,9 +30,11 @@
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.content.Context;
+import android.content.res.Resources;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.R;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.views.TaskViewType;
@@ -57,6 +59,8 @@
     @Mock
     private Context mContext;
     @Mock
+    private Resources mResources;
+    @Mock
     private SystemUiProxy mSystemUiProxy;
     @Mock
     private TopTaskTracker mTopTaskTracker;
@@ -69,6 +73,11 @@
         MockitoAnnotations.initMocks(this);
         LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class);
         KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
+
+        // Set desktop mode supported
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true);
+
         mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
                 mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index 1886ce6..a8f39af 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
@@ -31,6 +31,10 @@
 
     static final int STRESS_REPEAT_COUNT = 10;
 
+    private enum TestCase {
+        TO_HOME, TO_OVERVIEW,
+    }
+
     @Override
     @Before
     public void setUp() throws Exception {
@@ -41,28 +45,55 @@
     }
 
     @Test
-    @NavigationModeSwitch
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
     public void testStressPressHome() {
-        for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
-            // Destroy Launcher activity.
-            closeLauncherActivity();
-
-            // The test action.
-            mLauncher.goHome();
-        }
+        runTest(TestCase.TO_HOME);
     }
 
     @Test
-    @NavigationModeSwitch
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON)
+    public void testStressSwipeHome() {
+        runTest(TestCase.TO_HOME);
+    }
+
+    @Test
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
+    public void testStressPressOverview() {
+        runTest(TestCase.TO_OVERVIEW);
+    }
+
+    @Test
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON)
     public void testStressSwipeToOverview() {
+        runTest(TestCase.TO_OVERVIEW);
+    }
+
+    private void runTest(TestCase testCase) {
         for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
             // Destroy Launcher activity.
             closeLauncherActivity();
 
             // The test action.
-            mLauncher.getLaunchedAppState().switchToOverview();
+            switch (testCase) {
+                case TO_OVERVIEW:
+                    mLauncher.getLaunchedAppState().switchToOverview();
+                    break;
+                case TO_HOME:
+                    mLauncher.goHome();
+                    break;
+                default:
+                    throw new IllegalStateException("Cannot run test case: " + testCase);
+            }
         }
-        closeLauncherActivity();
-        mLauncher.goHome();
+        switch (testCase) {
+            case TO_OVERVIEW:
+                closeLauncherActivity();
+                mLauncher.goHome();
+                break;
+            case TO_HOME:
+            default:
+                // No-Op
+                break;
+        }
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
new file mode 100644
index 0000000..2122d9a
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep
+
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.IgnoreLimit
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.ui.AbstractLauncherUiTest
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape
+import com.android.launcher3.uioverrides.QuickstepLauncher
+import com.android.launcher3.util.rule.TestStabilityRule
+import com.android.launcher3.util.rule.TestStabilityRule.LOCAL
+import com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+
+/** Test Desktop windowing in Overview. */
+@AllowedDevices(allowed = [DeviceProduct.CF_TABLET, DeviceProduct.TANGORPRO])
+@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
+class TaplTestsOverviewDesktop : AbstractLauncherUiTest<QuickstepLauncher?>() {
+    @Before
+    fun setup() {
+        val overview = mLauncher.goHome().switchToOverview()
+        if (overview.hasTasks()) {
+            overview.dismissAllTasks()
+        }
+        startTestAppsWithCheck()
+        mLauncher.goHome()
+    }
+
+    @TestStabilityRule.Stability(flavors = LOCAL or PLATFORM_POSTSUBMIT)
+    @Test
+    @PortraitLandscape
+    fun enterDesktopViaOverviewMenu() {
+        // Move last launched TEST_ACTIVITY_2 into Desktop
+        mLauncher.workspace
+            .switchToOverview()
+            .getTestActivityTask(TEST_ACTIVITY_2)
+            .tapMenu()
+            .tapDesktopMenuItem()
+        assertTestAppLaunched(TEST_ACTIVITY_2)
+
+        // Scroll back to TEST_ACTIVITY_1, then move it into Desktop
+        mLauncher
+            .goHome()
+            .switchToOverview()
+            .apply { flingForward() }
+            .getTestActivityTask(TEST_ACTIVITY_1)
+            .tapMenu()
+            .tapDesktopMenuItem()
+        TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+
+        // Launch static DesktopTaskView
+        val desktop =
+            mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+        TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+
+        // Launch live-tile DesktopTaskView
+        desktop.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+        TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+    }
+
+    private fun startTestAppsWithCheck() {
+        TEST_ACTIVITIES.forEach {
+            startTestActivity(it)
+            executeOnLauncher { launcher ->
+                assertWithMessage(
+                        "Launcher activity is the top activity; expecting TestActivity$it"
+                    )
+                    .that(isInLaunchedApp(launcher))
+                    .isTrue()
+            }
+        }
+    }
+
+    private fun assertTestAppLaunched(index: Int) {
+        assertWithMessage("TestActivity$index not opened in Desktop")
+            .that(
+                mDevice.wait(
+                    Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")),
+                    DEFAULT_UI_TIMEOUT
+                )
+            )
+            .isTrue()
+    }
+
+    companion object {
+        const val TEST_ACTIVITY_1 = 2
+        const val TEST_ACTIVITY_2 = 3
+        val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2)
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
index 2d79623..3a83ae3 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep;
 
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
@@ -71,7 +71,7 @@
         final ArgumentCaptor<ActivityOptions> optionsCaptor =
                 ArgumentCaptor.forClass(ActivityOptions.class);
         verify(mSystemUiProxy).startRecentsActivity(any(), optionsCaptor.capture(), any());
-        assertTrue(optionsCaptor.getValue()
-                .isPendingIntentBackgroundActivityLaunchAllowedByPermission());
+        assertEquals(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS,
+                optionsCaptor.getValue().getPendingIntentBackgroundActivityStartMode());
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
index 512557b..dc1da69 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
@@ -88,18 +88,6 @@
     }
 
     @Test
-    public void showBorderOnHoverEvent() {
-        mTaskView.setBorderEnabled(/* enabled= */ true);
-        MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
-        mTaskView.onHoverEvent(MotionEvent.obtain(event));
-        verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
-                true);
-        mTaskView.onFocusChanged(true, 0, new Rect());
-        verify(mFocusAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
-                true);
-    }
-
-    @Test
     public void showBorderOnBorderEnabled() {
         presetBorderStatus(/* enabled= */ false);
         mTaskView.setBorderEnabled(/* enabled= */ true);
diff --git a/res/color-night-v31/material_color_surface_container_lowest.xml b/res/color-night-v31/material_color_surface_container_lowest.xml
new file mode 100644
index 0000000..4396f6d
--- /dev/null
+++ b/res/color-night-v31/material_color_surface_container_lowest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@android:color/system_neutral1_500" android:lStar="4" />
+</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_lowest.xml b/res/color-v31/material_color_surface_container_lowest.xml
new file mode 100644
index 0000000..f726aea
--- /dev/null
+++ b/res/color-v31/material_color_surface_container_lowest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@android:color/system_neutral1_500" android:lStar="100" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable/ic_bubble_button.xml b/res/drawable/ic_bubble_button.xml
new file mode 100644
index 0000000..1ed212e
--- /dev/null
+++ b/res/drawable/ic_bubble_button.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="20dp"
+        android:height="20dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M23,5v8h-2V5H3v14h10v2v0H3c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h18C22.1,3 23,3.9 23,5zM10,8v2.59L5.71,6.29L4.29,7.71L8.59,12H6v2h6V8H10zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/>
+</vector>
diff --git a/res/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml
index 12c4a82..cfec2b1 100644
--- a/res/drawable/private_space_install_app_icon.xml
+++ b/res/drawable/private_space_install_app_icon.xml
@@ -23,9 +23,9 @@
             android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z" />
         <path
             android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z"
-            android:fillColor="?attr/materialColorSurfaceContainerLowest" />
+            android:fillColor="@color/material_color_surface_container_lowest" />
         <path
             android:pathData="M29 31h-6v-2h6v-6h2v6h6v2h-6v6h-2v-6Z"
-            android:fillColor="?attr/materialColorOnSurface" />
+            android:fillColor="@color/material_color_on_surface" />
     </group>
 </vector>
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index 07c450e..0f630e5 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -57,4 +57,6 @@
         @android:color/system_accent1_200</color>
     <color name="work_fab_icon_color">
         @android:color/system_accent1_900</color>
+
+    <color name="material_color_on_surface">@android:color/system_neutral1_100</color>
 </resources>
\ No newline at end of file
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
new file mode 100644
index 0000000..887a2a5
--- /dev/null
+++ b/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="material_color_surface_container_lowest">#0D0E11</color>
+    <color name="material_color_on_surface">#E3E2E6</color>
+</resources>
\ No newline at end of file
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index 5c81d49..a5cdfc7 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -110,4 +110,6 @@
         @android:color/system_accent1_900</color>
 
     <color name="overview_foreground_scrim_color">@android:color/system_neutral1_1000</color>
+
+    <color name="material_color_on_surface">@android:color/system_neutral1_900</color>
 </resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index c82358b..3f8bede 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -142,6 +142,9 @@
     <color name="widget_cell_title_color_dark">@color/system_on_surface_dark</color>
     <color name="widget_cell_subtitle_color_dark">@color/system_on_surface_variant_dark</color>
 
+    <color name="material_color_surface_container_lowest">#FFFFFF</color>
+    <color name="material_color_on_surface">#1B1B1F</color>
+
     <color name="system_primary_container_light">#D9E2FF</color>
     <color name="system_on_primary_container_light">#001945</color>
     <color name="system_primary_light">#475D92</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index af91b5a..5e1d8a5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -306,6 +306,7 @@
     <dimen name="blur_size_medium_outline">2dp</dimen>
     <dimen name="blur_size_click_shadow">4dp</dimen>
     <dimen name="click_shadow_high_shift">2dp</dimen>
+    <dimen name="app_title_icon_shadow_inset">1dp</dimen>
 
     <!-- Pending widget -->
     <dimen name="pending_widget_min_padding">8dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3b458c2..fd724a5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -215,7 +215,8 @@
     <string name="dismiss_prediction_label">Don\'t suggest app</string>
     <!-- Label for pinning predicted app. -->
     <string name="pin_prediction">Pin Prediction</string>
-
+    <!-- Label for bubbling a launcher item. [CHAR_LIMIT=20] -->
+    <string name="bubble">Bubble</string>
 
     <!-- Permissions: -->
     <skip />
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 8585b66..177b28c 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -140,15 +140,11 @@
     }
 
     protected void onDeviceProfileInitiated() {
-        if (mDeviceProfile.isVerticalBarLayout()) {
-            mDeviceProfile.updateIsSeascape(this);
-        }
     }
 
     @Override
     public void onDisplayInfoChanged(Context context, Info info, int flags) {
         if ((flags & CHANGE_ROTATION) != 0 && mDeviceProfile.isVerticalBarLayout()) {
-            mDeviceProfile.updateIsSeascape(this);
             reapplyUi();
         }
     }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 5134dbe..1eccbff 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -504,7 +504,7 @@
             mLastOriginalText = label;
             mLastModifiedText = mLastOriginalText;
             mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER);
-            if (Flags.enableNewArchivingIcon()
+            if (Flags.useNewIconForArchivedApps()
                     && info instanceof ItemInfoWithIcon infoWithIcon
                     && infoWithIcon.isInactiveArchive()) {
                 setTextWithArchivingIcon(label);
@@ -820,7 +820,7 @@
                     getLineSpacingExtra());
             if (!TextUtils.equals(modifiedString, mLastModifiedText)) {
                 mLastModifiedText = modifiedString;
-                if (Flags.enableNewArchivingIcon()
+                if (Flags.useNewIconForArchivedApps()
                         && getTag() instanceof ItemInfoWithIcon infoWithIcon
                         && infoWithIcon.isInactiveArchive()) {
                     setTextWithArchivingIcon(modifiedString);
@@ -873,7 +873,7 @@
         if (drawable == null) {
             setText(text);
             Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
-                    + ", will just set text instead. text=" + text);
+                    + ", will just set text instead.");
             return;
         }
         drawable.setTint(getCurrentTextColor());
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 1a5f4b9..5cbf6fb 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -65,7 +65,6 @@
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType;
 import com.android.launcher3.responsive.ResponsiveSpecsProvider;
 import com.android.launcher3.util.CellContentDimensions;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.IconSizeSteps;
 import com.android.launcher3.util.ResourceHelper;
@@ -298,9 +297,6 @@
     // the widgetView, such that the actual view size is same as the widget size.
     public final Rect widgetPadding = new Rect();
 
-    // When true, nav bar is on the left side of the screen.
-    private boolean mIsSeascape;
-
     // Notification dots
     public final DotRenderer mDotRendererWorkSpace;
     public final DotRenderer mDotRendererAllApps;
@@ -2007,25 +2003,8 @@
         return isLandscape && transposeLayoutWithOrientation;
     }
 
-    /**
-     * Updates orientation information and returns true if it has changed from the previous value.
-     */
-    public boolean updateIsSeascape(Context context) {
-        if (isVerticalBarLayout()) {
-            boolean isSeascape = DisplayController.INSTANCE.get(context)
-                    .getInfo().rotation == Surface.ROTATION_270;
-            if (mIsSeascape != isSeascape) {
-                mIsSeascape = isSeascape;
-                // Hotseat changing sides requires updating workspace left/right paddings
-                updateWorkspacePadding();
-                return true;
-            }
-        }
-        return false;
-    }
-
     public boolean isSeascape() {
-        return isVerticalBarLayout() && mIsSeascape;
+        return rotationHint == Surface.ROTATION_270 && isVerticalBarLayout();
     }
 
     public boolean shouldFadeAdjacentWorkspaceScreens() {
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index 960d77a..de1748b 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -20,7 +20,6 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -29,7 +28,6 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 
@@ -189,10 +187,6 @@
      * Scrolls this recycler view to the top.
      */
     public void scrollToTop() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PRIVATE_SPACE_SCROLL_FAILURE, "FastScrollRecyclerView#scrollToTop",
-                    new Exception());
-        }
         if (mScrollbar != null) {
             mScrollbar.reattachThumbToScroll();
         }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 08ccfb2..15641ab 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -115,7 +115,7 @@
         if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
             ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
             params.setEnableUnarchivalConfirmation(false);
-            params.setEnableIconOverlay(!Flags.enableNewArchivingIcon());
+            params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps());
             launcherApps.setArchiveCompatibility(params);
         }
 
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 5d03a93..a3cabc2 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
@@ -295,6 +296,14 @@
             if (Flags.enablePrivateSpace()) {
                 position = addPrivateSpaceItems(position);
             }
+            if (!mFastScrollerSections.isEmpty()) {
+                // After all the adapterItems are added, add a view to the bottom so that user can
+                // scroll all the way down.
+                mAdapterItems.add(new AdapterItem(VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO));
+                mFastScrollerSections.add(new FastScrollSectionInfo(
+                        mFastScrollerSections.get(mFastScrollerSections.size() - 1).sectionName,
+                        position++));
+            }
         }
         mAccessibilityResultsCount = (int) mAdapterItems.stream()
                 .filter(AdapterItem::isCountedForAccessibility).count();
@@ -403,9 +412,8 @@
             // Apply decorator to private apps.
             if (hasPrivateApps) {
                 mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info,
-                        new SectionDecorationInfo(mActivityContext.getApplicationContext(),
-                                getRoundRegions(i, appList.size()),
-                                true /* decorateTogether */)));
+                        new SectionDecorationInfo(mActivityContext,
+                                getRoundRegions(i, appList.size()), true /* decorateTogether */)));
             } else {
                 mAdapterItems.add(AdapterItem.asApp(info));
             }
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 4b38df8..60bf3ea 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -68,7 +68,8 @@
     public static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 5;
     public static final int VIEW_TYPE_PRIVATE_SPACE_HEADER = 1 << 6;
     public static final int VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER = 1 << 7;
-    public static final int NEXT_ID = 8;
+    public static final int VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO = 1 << 8;
+    public static final int NEXT_ID = 9;
 
     // Common view type masks
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
@@ -247,6 +248,8 @@
             case VIEW_TYPE_PRIVATE_SPACE_HEADER:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.private_space_header, parent, false));
+            case VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO:
+                return new ViewHolder(new View(mActivityContext));
             default:
                 if (mAdapterProvider.isViewSupported(viewType)) {
                     return mAdapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType);
@@ -324,6 +327,7 @@
                         == STATE_DISABLED ? null : new SectionDecorationInfo(mActivityContext,
                         ROUND_NOTHING, true /* decorateTogether */);
                 break;
+            case VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO:
             case VIEW_TYPE_ALL_APPS_DIVIDER:
             case VIEW_TYPE_WORK_DISABLED_CARD:
                 // nothing to do
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index ad32fc2..f54fc57 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -213,9 +213,13 @@
         Collections.sort(hotseatToBeAdded);
         Collections.sort(workspaceToBeAdded);
 
+        List<Integer> idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect(
+                Collectors.toList());
+        idsInUse.addAll(dstHotseatItems.stream().map(entry -> entry.id).toList());
+
         // Migrate hotseat
         solveHotseatPlacement(helper, destHotseatSize,
-                srcReader, destReader, dstHotseatItems, hotseatToBeAdded);
+                srcReader, destReader, dstHotseatItems, hotseatToBeAdded, idsInUse);
 
         // Migrate workspace.
         // First we create a collection of the screens
@@ -230,7 +234,7 @@
                 Log.d(TAG, "Migrating " + screenId);
             }
             solveGridPlacement(helper, srcReader,
-                    destReader, screenId, trgX, trgY, workspaceToBeAdded);
+                    destReader, screenId, trgX, trgY, workspaceToBeAdded, idsInUse);
             if (workspaceToBeAdded.isEmpty()) {
                 break;
             }
@@ -241,7 +245,7 @@
         int screenId = destReader.mLastScreenId + 1;
         while (!workspaceToBeAdded.isEmpty()) {
             solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY,
-                    workspaceToBeAdded);
+                    workspaceToBeAdded, srcWorkspaceItems.stream().map(entry -> entry.id).toList());
             screenId++;
         }
 
@@ -257,47 +261,57 @@
     private static void calcDiff(@NonNull final List<DbEntry> src,
             @NonNull final List<DbEntry> dest, @NonNull final List<DbEntry> toBeAdded,
             @NonNull final IntArray toBeRemoved) {
+        HashMap<DbEntry, Integer> entryCountDiff = new HashMap<>();
+        src.forEach(entry ->
+                entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) + 1));
+        dest.forEach(entry ->
+                entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) - 1));
+
         src.forEach(entry -> {
-            if (!dest.contains(entry)) {
+            if (entryCountDiff.get(entry) > 0) {
                 toBeAdded.add(entry);
+                entryCountDiff.put(entry, entryCountDiff.get(entry) - 1);
             }
         });
+
         dest.forEach(entry -> {
-            if (!src.contains(entry)) {
+            if (entryCountDiff.get(entry) < 0) {
                 toBeRemoved.add(entry.id);
                 if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
                     entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add));
                 }
+                entryCountDiff.put(entry, entryCountDiff.get(entry) + 1);
             }
         });
     }
 
     private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
-            String srcTableName, String destTableName) {
-        int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName);
-
+            String srcTableName, String destTableName, List<Integer> idsInUse) {
+        int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName, idsInUse);
         if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER
                 || entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) {
             for (Set<Integer> itemIds : entry.mFolderItems.values()) {
                 for (int itemId : itemIds) {
-                    copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName);
+                    copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName, idsInUse);
                 }
             }
         }
     }
 
     private static int copyEntryAndUpdate(DatabaseHelper helper,
-            DbEntry entry, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(helper, entry, -1, -1, srcTableName, destTableName);
+            DbEntry entry, String srcTableName, String destTableName, List<Integer> idsInUse) {
+        return copyEntryAndUpdate(
+                helper, entry, -1, -1, srcTableName, destTableName, idsInUse);
     }
 
-    private static int copyEntryAndUpdate(DatabaseHelper helper,
-            int id, int folderId, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(helper, null, id, folderId, srcTableName, destTableName);
+    private static int copyEntryAndUpdate(DatabaseHelper helper, int id,
+            int folderId, String srcTableName, String destTableName, List<Integer> idsInUse) {
+        return copyEntryAndUpdate(
+                helper, null, id, folderId, srcTableName, destTableName, idsInUse);
     }
 
-    private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry,
-            int id, int folderId, String srcTableName, String destTableName) {
+    private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry, int id,
+            int folderId, String srcTableName, String destTableName, List<Integer> idsInUse) {
         int newId = -1;
         Cursor c = helper.getWritableDatabase().query(srcTableName, null,
                 LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'",
@@ -311,6 +325,9 @@
                 values.put(LauncherSettings.Favorites.CONTAINER, folderId);
             }
             newId = helper.generateNewItemId();
+            while (idsInUse.contains(newId)) {
+                newId = helper.generateNewItemId();
+            }
             values.put(LauncherSettings.Favorites._ID, newId);
             helper.getWritableDatabase().insert(destTableName, null, values);
         }
@@ -343,7 +360,7 @@
     private static void solveGridPlacement(@NonNull final DatabaseHelper helper,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
             final int screenId, final int trgX, final int trgY,
-            @NonNull final List<DbEntry> sortedItemsToPlace) {
+            @NonNull final List<DbEntry> sortedItemsToPlace, List<Integer> idsInUse) {
         final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
         final Point trg = new Point(trgX, trgY);
         final Point next = new Point(0, screenId == 0
@@ -366,7 +383,8 @@
                 continue;
             }
             if (findPlacementForEntry(entry, next, trg, occupied, screenId)) {
-                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(
+                        helper, entry, srcReader.mTableName, destReader.mTableName, idsInUse);
                 iterator.remove();
             }
         }
@@ -407,7 +425,7 @@
             @NonNull final DatabaseHelper helper, final int hotseatSize,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
             @NonNull final  List<DbEntry> placedHotseatItems,
-            @NonNull final List<DbEntry> itemsToPlace) {
+            @NonNull final List<DbEntry> itemsToPlace, List<Integer> idsInUse) {
 
         final boolean[] occupied = new boolean[hotseatSize];
         for (DbEntry entry : placedHotseatItems) {
@@ -422,7 +440,8 @@
                 // to something other than -1.
                 entry.cellX = i;
                 entry.cellY = 0;
-                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(
+                        helper, entry, srcReader.mTableName, destReader.mTableName, idsInUse);
                 occupied[entry.screenId] = true;
             }
         }
@@ -564,12 +583,12 @@
                         }
                         case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: {
                             entry.mProvider = c.getString(indexAppWidgetProvider);
+                            entry.appWidgetId = c.getInt(indexAppWidgetId);
                             ComponentName cn = ComponentName.unflattenFromString(entry.mProvider);
                             verifyPackage(cn.getPackageName());
 
-                            int widgetId = c.getInt(indexAppWidgetId);
                             LauncherAppWidgetProviderInfo pInfo = widgetManagerHelper
-                                    .getLauncherAppWidgetInfo(widgetId, cn);
+                                    .getLauncherAppWidgetInfo(entry.appWidgetId, cn);
                             Point spans = null;
                             if (pInfo != null) {
                                 spans = pInfo.getMinSpans();
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 7e1d40d..da1a221 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -83,6 +83,7 @@
 
 import org.xmlpull.v1.XmlPullParser;
 
+import java.io.File;
 import java.io.InputStream;
 import java.io.StringReader;
 
@@ -104,10 +105,30 @@
         mContext = context;
     }
 
+    private void printDBs(String prefix) {
+        try {
+            File directory = new File(
+                    mContext.getDatabasePath(InvariantDeviceProfile.INSTANCE.get(mContext).dbFile)
+                            .getParent()
+            );
+            if (directory.exists()) {
+                for (File file : directory.listFiles()) {
+                    Log.d("b/353505773", prefix + "Database file: " + file.getName());
+                }
+            } else {
+                Log.d("b/353505773", prefix + "No files found in the database directory");
+            }
+        } catch (Exception e) {
+            Log.e("b/353505773", prefix + e.getMessage());
+        }
+    }
+
     private synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
             mOpenHelper = createDatabaseHelper(false /* forMigration */);
+            printDBs("before: ");
             RestoreDbTask.restoreIfNeeded(mContext, this);
+            printDBs("after: ");
         }
     }
 
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index 35064cf..c949ce6 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -89,7 +89,7 @@
         if (!WIDGETS_ENABLED) {
             return Collections.emptyMap();
         }
-        return mWidgetsByPackageItem;
+        return new HashMap<>(mWidgetsByPackageItem);
     }
 
     /**
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index 40e3813..f31bf1e 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -25,6 +25,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherSettings;
@@ -97,6 +98,8 @@
 
     public int options;
 
+    @Nullable
+    private ShortcutInfo mShortcutInfo = null;
 
     public WorkspaceItemInfo() {
         itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
@@ -175,6 +178,9 @@
 
     public void updateFromDeepShortcutInfo(@NonNull final ShortcutInfo shortcutInfo,
             @NonNull final Context context) {
+        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+            mShortcutInfo = shortcutInfo;
+        }
         // {@link ShortcutInfo#getActivity} can change during an update. Recreate the intent
         intent = ShortcutKey.makeIntent(shortcutInfo);
         title = shortcutInfo.getShortLabel();
@@ -204,6 +210,11 @@
             : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);
     }
 
+    @Nullable
+    public ShortcutInfo getDeepShortcutInfo() {
+        return mShortcutInfo;
+    }
+
     /**
      * {@code true} if the shortcut is disabled due to its app being a lower version.
      */
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index f7e1168..0c90eb9 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -11,9 +11,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Process;
 import android.os.UserHandle;
+import android.util.Log;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
@@ -25,6 +27,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.AbstractFloatingViewHelper;
 import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.SecondaryDropTarget;
 import com.android.launcher3.Utilities;
@@ -53,6 +56,7 @@
  */
 public abstract class SystemShortcut<T extends ActivityContext> extends ItemInfo
         implements View.OnClickListener {
+    private static final String TAG = "SystemShortcut";
 
     private final int mIconResId;
     protected final int mLabelResId;
@@ -383,4 +387,63 @@
         mAbstractFloatingViewHelper.closeOpenViews(mTarget, true,
                 AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
     }
+
+    public static final Factory<ActivityContext> BUBBLE_SHORTCUT =
+            (activity, itemInfo, originalView) -> {
+                if ((itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                        && (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION)
+                        && !(itemInfo instanceof WorkspaceItemInfo)) {
+                    return null;
+                }
+                return new BubbleShortcut(activity, itemInfo, originalView);
+            };
+
+    public interface BubbleActivityStarter {
+        /** Tell SysUI to show the provided shortcut in a bubble. */
+        void showShortcutBubble(ShortcutInfo info);
+
+        /** Tell SysUI to show the provided intent in a bubble. */
+        void showAppBubble(Intent intent);
+    }
+
+    public static class BubbleShortcut<T extends ActivityContext> extends SystemShortcut<T> {
+
+        private BubbleActivityStarter mStarter;
+
+        public BubbleShortcut(T target, ItemInfo itemInfo, View originalView) {
+            super(R.drawable.ic_bubble_button, R.string.bubble, target,
+                    itemInfo, originalView);
+            if (target instanceof BubbleActivityStarter) {
+                mStarter = (BubbleActivityStarter) target;
+            }
+        }
+
+        @Override
+        public void onClick(View view) {
+            dismissTaskMenuView();
+            if (mStarter == null) {
+                Log.w(TAG, "starter null!");
+                return;
+            }
+            // TODO: handle GroupTask (single) items so that recent items in taskbar work
+            if (mItemInfo instanceof WorkspaceItemInfo) {
+                WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo;
+                ShortcutInfo shortcutInfo = workspaceItemInfo.getDeepShortcutInfo();
+                if (shortcutInfo != null) {
+                    mStarter.showShortcutBubble(shortcutInfo);
+                    return;
+                }
+            }
+            // If we're here check for an intent
+            Intent intent = mItemInfo.getIntent();
+            if (intent != null) {
+                if (intent.getPackage() == null) {
+                    intent.setPackage(mItemInfo.getTargetPackage());
+                }
+                mStarter.showAppBubble(intent);
+            } else {
+                Log.w(TAG, "unable to bubble, no intent: " + mItemInfo);
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index f895b30..e317824 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -17,13 +17,12 @@
 package com.android.launcher3.recyclerview
 
 import android.content.Context
-import android.view.ViewGroup
-import androidx.annotation.VisibleForTesting
-import androidx.annotation.VisibleForTesting.Companion.PROTECTED
+import android.util.Log
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.android.launcher3.BubbleTextView
+import com.android.launcher3.BuildConfig
 import com.android.launcher3.allapps.BaseAllAppsAdapter
 import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.util.CancellableTask
@@ -41,18 +40,36 @@
  * [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
  * will be added to [RecycledViewPool] on main thread.
  */
-class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
 
     var hasWorkProfile = false
-    @VisibleForTesting(otherwise = PROTECTED)
-    var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+    private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+
+    companion object {
+        private const val TAG = "AllAppsRecyclerViewPool"
+        private const val NULL_LAYOUT_MANAGER_ERROR_STRING =
+            "activeRv's layoutManager should not be null"
+    }
 
     /**
      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
      */
-    fun preInflateAllAppsViewHolders(context: T) {
+    fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
         val appsView = context.appsView ?: return
         val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
+        val preInflateCount = getPreinflateCount(context)
+        if (preInflateCount <= 0) {
+            return
+        }
+
+        if (activeRv.layoutManager == null) {
+            if (BuildConfig.IS_STUDIO_BUILD) {
+                throw IllegalStateException(NULL_LAYOUT_MANAGER_ERROR_STRING)
+            } else {
+                Log.e(TAG, NULL_LAYOUT_MANAGER_ERROR_STRING)
+            }
+            return
+        }
 
         // Create a separate context dedicated for all apps preinflation thread. The goal is to
         // create a separate AssetManager obj internally to avoid lock contention with
@@ -81,47 +98,31 @@
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
-        preInflateAllAppsViewHolders(adapter, BaseAllAppsAdapter.VIEW_TYPE_ICON, activeRv) {
-            getPreinflateCount(context)
-        }
-    }
-
-    @VisibleForTesting(otherwise = PROTECTED)
-    fun preInflateAllAppsViewHolders(
-        adapter: RecyclerView.Adapter<*>,
-        viewType: Int,
-        parent: ViewGroup,
-        preInflationCountProvider: () -> Int
-    ) {
-        val preinflationCount = preInflationCountProvider.invoke()
-        if (preinflationCount <= 0) {
-            return
-        }
         mCancellableTask?.cancel()
         var task: CancellableTask<List<ViewHolder>>? = null
         task =
             CancellableTask(
                 {
                     val list: ArrayList<ViewHolder> = ArrayList()
-                    for (i in 0 until preinflationCount) {
+                    for (i in 0 until preInflateCount) {
                         if (task?.canceled == true) {
                             break
                         }
-                        list.add(adapter.createViewHolder(parent, viewType))
+                        list.add(
+                            adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+                        )
                     }
                     list
                 },
                 MAIN_EXECUTOR,
                 { viewHolders ->
-                    // Run preInflationCountProvider again as the needed VH might have changed
-                    val newPreinflationCount = preInflationCountProvider.invoke()
-                    for (i in 0 until minOf(viewHolders.size, newPreinflationCount)) {
+                    for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
                         putRecycledView(viewHolders[i])
                     }
                 }
             )
         mCancellableTask = task
-        VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
+        VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
     }
 
     /**
@@ -142,7 +143,7 @@
      * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
      * icons.
      */
-    fun getPreinflateCount(context: T): Int {
+    fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
         var targetPreinflateCount =
             PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
                 EXTRA_ICONS_COUNT
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index 95624b1..837d7bc 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -186,12 +186,6 @@
         public int stagePosition = STAGE_POSITION_UNDEFINED;
         @StageType
         public int stageType = STAGE_TYPE_UNDEFINED;
-
-        @Override
-        public String toString() {
-            return "SplitStageInfo { taskId=" + taskId
-                    + ", stagePosition=" + stagePosition + ", stageType=" + stageType + " }";
-        }
     }
 
     public static StatsLogManager.EventEnum getLogEventForPosition(@StagePosition int position) {
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index bc66a33..ef66ffe 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -19,11 +19,19 @@
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.content.Context;
-import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ImageSpan;
 import android.util.AttributeSet;
-import android.widget.TextView;
+import android.util.Log;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
@@ -45,22 +53,65 @@
 
     public DoubleShadowBubbleTextView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mShadowInfo = new ShadowInfo(context, attrs, defStyle);
-        setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0, mShadowInfo.ambientShadowColor);
+        mShadowInfo = ShadowInfo.Companion.fromContext(context, attrs, defStyle);
+        setShadowLayer(
+                mShadowInfo.getAmbientShadowBlur(),
+                0,
+                0,
+                mShadowInfo.getAmbientShadowColor()
+        );
+    }
+
+    @Override
+    public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) {
+        Drawable drawable = getContext().getDrawable(drawableId);
+        if (drawable == null) {
+            setText(text);
+            Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
+                    + ", will just set text instead.");
+            return;
+        }
+        drawable.setTint(getCurrentTextColor());
+        int textSize = Math.round(getTextSize());
+        ImageSpan imageSpan;
+        if (!skipDoubleShadow() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            drawable = getDoubleShadowDrawable(drawable, textSize);
+        }
+        drawable.setBounds(0, 0, textSize, textSize);
+        imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER);
+        // First space will be replaced with Drawable, second space is for space before text.
+        SpannableString spannable = new SpannableString("  " + text);
+        spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+        setText(spannable);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    private DoubleShadowIconDrawable getDoubleShadowDrawable(
+            @NonNull Drawable drawable, int textSize
+    ) {
+        // add some padding via inset to avoid shadow clipping
+        int iconInsetSize = getContext().getResources()
+                .getDimensionPixelSize(R.dimen.app_title_icon_shadow_inset);
+        return new DoubleShadowIconDrawable(
+                mShadowInfo,
+                drawable,
+                textSize,
+                iconInsetSize
+        );
     }
 
     @Override
     public void onDraw(Canvas canvas) {
         // If text is transparent or shadow alpha is 0, don't draw any shadow
-        if (mShadowInfo.skipDoubleShadow(this)) {
+        if (skipDoubleShadow()) {
             super.onDraw(canvas);
             return;
         }
         int alpha = Color.alpha(getCurrentTextColor());
 
         // We enhance the shadow by drawing the shadow twice
-        getPaint().setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0,
-                getTextShadowColor(mShadowInfo.ambientShadowColor, alpha));
+        getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+                getTextShadowColor(mShadowInfo.getAmbientShadowColor(), alpha));
 
         drawWithoutDot(canvas);
         canvas.save();
@@ -69,10 +120,10 @@
                 getScrollY() + getHeight());
 
         getPaint().setShadowLayer(
-                mShadowInfo.keyShadowBlur,
-                mShadowInfo.keyShadowOffsetX,
-                mShadowInfo.keyShadowOffsetY,
-                getTextShadowColor(mShadowInfo.keyShadowColor, alpha));
+                mShadowInfo.getKeyShadowBlur(),
+                mShadowInfo.getKeyShadowOffsetX(),
+                mShadowInfo.getKeyShadowOffsetY(),
+                getTextShadowColor(mShadowInfo.getKeyShadowColor(), alpha));
         drawWithoutDot(canvas);
         canvas.restore();
 
@@ -80,55 +131,30 @@
         drawRunningAppIndicatorIfNecessary(canvas);
     }
 
-    public static class ShadowInfo {
-        public final float ambientShadowBlur;
-        public final int ambientShadowColor;
-
-        public final float keyShadowBlur;
-        public final float keyShadowOffsetX;
-        public final float keyShadowOffsetY;
-        public final int keyShadowColor;
-
-        public ShadowInfo(Context c, AttributeSet attrs, int defStyle) {
-
-            TypedArray a = c.obtainStyledAttributes(
-                    attrs, R.styleable.ShadowInfo, defStyle, 0);
-
-            ambientShadowBlur = a.getDimensionPixelSize(
-                    R.styleable.ShadowInfo_ambientShadowBlur, 0);
-            ambientShadowColor = a.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0);
-
-            keyShadowBlur = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0);
-            keyShadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0);
-            keyShadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0);
-            keyShadowColor = a.getColor(R.styleable.ShadowInfo_keyShadowColor, 0);
-            a.recycle();
-        }
-
-        public boolean skipDoubleShadow(TextView textView) {
-            int textAlpha = Color.alpha(textView.getCurrentTextColor());
-            int keyShadowAlpha = Color.alpha(keyShadowColor);
-            int ambientShadowAlpha = Color.alpha(ambientShadowColor);
-            if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
-                textView.getPaint().clearShadowLayer();
-                return true;
-            } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
-                textView.getPaint().setShadowLayer(ambientShadowBlur, 0, 0,
-                        getTextShadowColor(ambientShadowColor, textAlpha));
-                return true;
-            } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
-                textView.getPaint().setShadowLayer(
-                        keyShadowBlur,
-                        keyShadowOffsetX,
-                        keyShadowOffsetY,
-                        getTextShadowColor(keyShadowColor, textAlpha));
-                return true;
-            } else {
-                return false;
-            }
+    private boolean skipDoubleShadow() {
+        int textAlpha = Color.alpha(getCurrentTextColor());
+        int keyShadowAlpha = Color.alpha(mShadowInfo.getKeyShadowColor());
+        int ambientShadowAlpha = Color.alpha(mShadowInfo.getAmbientShadowColor());
+        if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
+            getPaint().clearShadowLayer();
+            return true;
+        } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
+            getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+                    getTextShadowColor(mShadowInfo.getAmbientShadowColor(), textAlpha));
+            return true;
+        } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
+            getPaint().setShadowLayer(
+                    mShadowInfo.getKeyShadowBlur(),
+                    mShadowInfo.getKeyShadowOffsetX(),
+                    mShadowInfo.getKeyShadowOffsetY(),
+                    getTextShadowColor(mShadowInfo.getKeyShadowColor(), textAlpha));
+            return true;
+        } else {
+            return false;
         }
     }
 
+
     // Multiplies the alpha of shadowColor by textAlpha.
     private static int getTextShadowColor(int shadowColor, int textAlpha) {
         return setColorAlphaBound(shadowColor,
diff --git a/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
new file mode 100644
index 0000000..7ac7c94
--- /dev/null
+++ b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.views
+
+import android.content.res.ColorStateList
+import android.graphics.BlendMode
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.RenderEffect
+import android.graphics.RenderNode
+import android.graphics.Shader
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.InsetDrawable
+import android.os.Build.VERSION_CODES
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Launcher wrapper for Drawables to provide a double shadow effect. Currently for use with
+ * [DoubleShadowBubbleTextView] to provide a similar shadow to inline icons.
+ */
+@RequiresApi(VERSION_CODES.S)
+class DoubleShadowIconDrawable(
+    private val shadowInfo: ShadowInfo,
+    iconDrawable: Drawable,
+    private val iconSize: Int,
+    iconInsetSize: Int
+) : Drawable() {
+    private val mIconDrawable: InsetDrawable
+    private val mDoubleShadowNode: RenderNode?
+
+    init {
+        mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize)
+        mIconDrawable.setBounds(0, 0, iconSize, iconSize)
+        mDoubleShadowNode = createShadowRenderNode()
+    }
+
+    @VisibleForTesting
+    fun createShadowRenderNode(): RenderNode {
+        val renderNode = RenderNode("DoubleShadowNode")
+        renderNode.setPosition(0, 0, iconSize, iconSize)
+        // Create render effects
+        val ambientShadow =
+            createShadowRenderEffect(
+                shadowInfo.ambientShadowBlur,
+                0f,
+                0f,
+                Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+            )
+        val keyShadow =
+            createShadowRenderEffect(
+                shadowInfo.keyShadowBlur,
+                shadowInfo.keyShadowOffsetX,
+                shadowInfo.keyShadowOffsetY,
+                Color.alpha(shadowInfo.keyShadowColor).toFloat()
+            )
+        val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP)
+        renderNode.setRenderEffect(blend)
+        return renderNode
+    }
+
+    @VisibleForTesting
+    fun createShadowRenderEffect(
+        radius: Float,
+        offsetX: Float,
+        offsetY: Float,
+        alpha: Float
+    ): RenderEffect {
+        return RenderEffect.createColorFilterEffect(
+            PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY),
+            RenderEffect.createOffsetEffect(
+                offsetX,
+                offsetY,
+                RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
+            )
+        )
+    }
+
+    override fun draw(canvas: Canvas) {
+        if (canvas.isHardwareAccelerated && mDoubleShadowNode != null) {
+            if (!mDoubleShadowNode.hasDisplayList()) {
+                // Record render node if its display list is not recorded or discarded
+                // (which happens when it's no longer drawn by anything).
+                val recordingCanvas = mDoubleShadowNode.beginRecording()
+                mIconDrawable.draw(recordingCanvas)
+                mDoubleShadowNode.endRecording()
+            }
+            canvas.drawRenderNode(mDoubleShadowNode)
+        }
+        mIconDrawable.draw(canvas)
+    }
+
+    override fun getIntrinsicHeight() = iconSize
+
+    override fun getIntrinsicWidth() = iconSize
+
+    override fun getOpacity() = PixelFormat.TRANSPARENT
+
+    override fun setAlpha(alpha: Int) {
+        mIconDrawable.alpha = alpha
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        mIconDrawable.colorFilter = colorFilter
+    }
+
+    override fun setTint(color: Int) {
+        mIconDrawable.setTint(color)
+    }
+
+    override fun setTintList(tint: ColorStateList?) {
+        mIconDrawable.setTintList(tint)
+    }
+}
diff --git a/src/com/android/launcher3/views/ShadowInfo.kt b/src/com/android/launcher3/views/ShadowInfo.kt
new file mode 100644
index 0000000..4f626ec
--- /dev/null
+++ b/src/com/android/launcher3/views/ShadowInfo.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.views
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.launcher3.R
+
+/**
+ * Launcher data holder for classes such as [DoubleShadowBubbleTextView] to model shadows for
+ * "double shadow" effect.
+ */
+data class ShadowInfo(
+    val ambientShadowBlur: Float,
+    val ambientShadowColor: Int,
+    val keyShadowBlur: Float,
+    val keyShadowOffsetX: Float,
+    val keyShadowOffsetY: Float,
+    val keyShadowColor: Int
+) {
+
+    companion object {
+        /** Constructs instance of ShadowInfo from Context and given attribute set. */
+        @JvmStatic
+        fun fromContext(context: Context, attrs: AttributeSet?, defStyle: Int): ShadowInfo {
+            val styledAttrs =
+                context.obtainStyledAttributes(attrs, R.styleable.ShadowInfo, defStyle, 0)
+            val shadowInfo =
+                ShadowInfo(
+                    ambientShadowBlur =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+                            .toFloat(),
+                    ambientShadowColor =
+                        styledAttrs.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0),
+                    keyShadowBlur =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+                            .toFloat(),
+                    keyShadowOffsetX =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+                            .toFloat(),
+                    keyShadowOffsetY =
+                        styledAttrs
+                            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+                            .toFloat(),
+                    keyShadowColor = styledAttrs.getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+                )
+            styledAttrs.recycle()
+            return shadowInfo
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index 71d8503..91b899c 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.util.Executors;
@@ -77,6 +78,11 @@
         mViewToRecycle = viewToRecycle;
     }
 
+    @VisibleForTesting
+    @Nullable ListenableHostView getViewToRecycle() {
+        return mViewToRecycle;
+    }
+
     @Override
     @NonNull
     public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
diff --git a/tests/Android.bp b/tests/Android.bp
index e51242f..c99f656 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -71,6 +71,9 @@
 // Library with all the dependencies for building quickstep
 android_library {
     name: "Launcher3TestLib",
+    defaults: [
+        "launcher_compose_tests_defaults",
+    ],
     srcs: [],
     asset_dirs: ["assets"],
     resource_dirs: ["res"],
@@ -112,6 +115,9 @@
 
 android_test {
     name: "Launcher3Tests",
+    defaults: [
+        "launcher_compose_tests_defaults",
+    ],
     srcs: [
         ":launcher-tests-src",
         ":launcher-non-quickstep-tests-src",
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 dc3b321..3f4a73a 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -173,8 +173,6 @@
     public static final String TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE = "b/326908466";
     public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890";
     public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
-    public static final String PRIVATE_SPACE_SCROLL_FAILURE = "b/339737008";
-
     public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
     public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java b/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
index d2238ff..d938119 100644
--- a/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
@@ -65,6 +65,7 @@
     private static final int PRIVATE_SPACE_HEADER_ITEM_COUNT = 1;
     private static final int MAIN_USER_APP_COUNT = 2;
     private static final int PRIVATE_USER_APP_COUNT = 2;
+    private static final int VIEW_AT_END_OF_APP_LIST = 1;
     private static final int NUM_APP_COLS = 4;
     private static final int NUM_APP_ROWS = 3;
     private static final int PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT = 1;
@@ -107,7 +108,8 @@
                 && info.user.equals(MAIN_HANDLE));
 
         assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT
-                + PRIVATE_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size());
+                + PRIVATE_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
+                mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT,
                 mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
                         item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size());
@@ -136,7 +138,7 @@
                 && info.user.equals(MAIN_HANDLE));
 
         assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT
-                + PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT
+                + PRIVATE_SPACE_SYS_APP_SEPARATOR_ITEM_COUNT + VIEW_AT_END_OF_APP_LIST
                 + PRIVATE_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT,
                 mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
@@ -166,7 +168,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT,
+        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT +
+                        VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList
                 .getAdapterItems().stream().filter(item ->
@@ -187,8 +190,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT,
-                mAlphabeticalAppsList.getAdapterItems().size());
+        assertEquals(MAIN_USER_APP_COUNT + PRIVATE_SPACE_HEADER_ITEM_COUNT +
+                        VIEW_AT_END_OF_APP_LIST, mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(PRIVATE_SPACE_HEADER_ITEM_COUNT, mAlphabeticalAppsList
                 .getAdapterItems().stream().filter(item ->
                         item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size());
@@ -206,7 +209,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(MAIN_USER_APP_COUNT, mAlphabeticalAppsList.getAdapterItems().size());
+        assertEquals(MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
+                mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
                 item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER).toList().size());
         assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
@@ -222,7 +226,8 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        assertEquals(2, mAlphabeticalAppsList.getAdapterItems().size());
+        assertEquals(MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
+                mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(0, mAlphabeticalAppsList.getAdapterItems().stream().filter(item ->
                         item.itemInfo != null
                                 && item.itemInfo.itemType == VIEW_TYPE_PRIVATE_SPACE_HEADER)
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
index c32461e..a3c7f4f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
@@ -104,7 +104,7 @@
         val cl = cellLayoutBuilder.createCellLayout(board.width, board.height, false)
         // The views have to be sorted or the result can vary
         board.icons
-            .map(IconPoint::getCoord)
+            .map(IconPoint::coord)
             .sortedWith(
                 Comparator.comparing { p: Any -> (p as Point).x }
                     .thenComparing { p: Any -> (p as Point).y }
@@ -120,9 +120,7 @@
                 )
             }
         board.widgets
-            .sortedWith(
-                Comparator.comparing(WidgetRect::getCellX).thenComparing(WidgetRect::getCellY)
-            )
+            .sortedWith(Comparator.comparing(WidgetRect::cellX).thenComparing(WidgetRect::cellY))
             .forEach { widget ->
                 addViewInCellLayout(
                     cl,
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt
new file mode 100644
index 0000000..3cbfc5a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.celllayout.board
+
+import android.graphics.Point
+import android.graphics.Rect
+
+/** Represents a widget in a CellLayoutBoard */
+data class WidgetRect(
+    val type: Char,
+    val bounds: Rect,
+) {
+    val spanX: Int = bounds.right - bounds.left + 1
+    val spanY: Int = bounds.top - bounds.bottom + 1
+    val cellY: Int = bounds.bottom
+    val cellX: Int = bounds.left
+
+    fun shouldIgnore() = type == CellType.IGNORE
+
+    fun contains(x: Int, y: Int) = bounds.contains(x, y)
+}
+
+/**
+ * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+ * letter in the alphabet, A=2, B=3, C=4 ... etc.
+ */
+data class FolderPoint(val coord: Point, val type: Char) {
+    val numberIconsInside: Int = type.code - 'A'.code + 2
+}
+
+/** Represents an icon in a CellLayoutBoard */
+data class IconPoint(val coord: Point, val type: Char = CellType.ICON)
+
+object CellType {
+    // The cells marked by this will be filled by 1x1 widgets and will be ignored when
+    // validating
+    const val IGNORE = 'x'
+
+    // The cells marked by this will be filled by app icons
+    const val ICON = 'i'
+
+    // The cells marked by FOLDER will be filled by folders with 27 app icons inside
+    const val FOLDER = 'Z'
+
+    // Empty space
+    const val EMPTY = '-'
+
+    // Widget that will be saved as "main widget" for easier retrieval
+    const val MAIN_WIDGET = 'm' // Everything else will be consider a widget
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
index e5ad888..04bfee9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
@@ -88,7 +88,7 @@
 
     public WidgetRect getWidgetOfType(char type) {
         return mWidgetsRects.stream()
-                .filter(widgetRect -> widgetRect.mType == type).findFirst().orElse(null);
+                .filter(widgetRect -> widgetRect.getType() == type).findFirst().orElse(null);
     }
 
     public WidgetRect getWidgetAt(int x, int y) {
@@ -117,8 +117,8 @@
     }
 
     private void removeWidgetFromBoard(WidgetRect widget) {
-        for (int xi = widget.mBounds.left; xi <= widget.mBounds.right; xi++) {
-            for (int yi = widget.mBounds.bottom; yi <= widget.mBounds.top; yi++) {
+        for (int xi = widget.getBounds().left; xi <= widget.getBounds().right; xi++) {
+            for (int yi = widget.getBounds().bottom; yi <= widget.getBounds().top; yi++) {
                 mWidget[xi][yi] = '-';
             }
         }
@@ -127,7 +127,7 @@
     private void removeOverlappingItems(Rect rect) {
         // Remove overlapping widgets and remove them from the board
         mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
-            if (rect.intersect(widget.mBounds)) {
+            if (rect.intersect(widget.getBounds())) {
                 removeWidgetFromBoard(widget);
                 return false;
             }
@@ -135,8 +135,8 @@
         }).collect(Collectors.toList());
         // Remove overlapping icons and remove them from the board
         mIconPoints = mIconPoints.stream().filter(iconPoint -> {
-            int x = iconPoint.coord.x;
-            int y = iconPoint.coord.y;
+            int x = iconPoint.getCoord().x;
+            int y = iconPoint.getCoord().y;
             if (rect.contains(x, y)) {
                 mWidget[x][y] = '-';
                 return false;
@@ -146,8 +146,8 @@
 
         // Remove overlapping folders and remove them from the board
         mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
-            int x = folderPoint.coord.x;
-            int y = folderPoint.coord.y;
+            int x = folderPoint.getCoord().x;
+            int y = folderPoint.getCoord().y;
             if (rect.contains(x, y)) {
                 mWidget[x][y] = '-';
                 return false;
@@ -159,7 +159,7 @@
     private void removeOverlappingItems(Point p) {
         // Remove overlapping widgets and remove them from the board
         mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
-            if (IdenticalBoardComparator.Companion.touchesPoint(widget.mBounds, p)) {
+            if (IdenticalBoardComparator.Companion.touchesPoint(widget.getBounds(), p)) {
                 removeWidgetFromBoard(widget);
                 return false;
             }
@@ -167,8 +167,8 @@
         }).collect(Collectors.toList());
         // Remove overlapping icons and remove them from the board
         mIconPoints = mIconPoints.stream().filter(iconPoint -> {
-            int x = iconPoint.coord.x;
-            int y = iconPoint.coord.y;
+            int x = iconPoint.getCoord().x;
+            int y = iconPoint.getCoord().y;
             if (p.x == x && p.y == y) {
                 mWidget[x][y] = '-';
                 return false;
@@ -178,8 +178,8 @@
 
         // Remove overlapping folders and remove them from the board
         mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
-            int x = folderPoint.coord.x;
-            int y = folderPoint.coord.y;
+            int x = folderPoint.getCoord().x;
+            int y = folderPoint.getCoord().y;
             if (p.x == x && p.y == y) {
                 mWidget[x][y] = '-';
                 return false;
@@ -226,7 +226,7 @@
 
     public void removeItem(char type) {
         mWidgetsRects.stream()
-                .filter(widgetRect -> widgetRect.mType == type)
+                .filter(widgetRect -> widgetRect.getType() == type)
                 .forEach(widgetRect -> removeOverlappingItems(
                         new Point(widgetRect.getCellX(), widgetRect.getCellY())));
     }
@@ -365,10 +365,10 @@
         board.mWidth = lines[0].length();
         board.mWidgetsRects = getRects(board.mWidget);
         board.mWidgetsRects.forEach(widgetRect -> {
-            if (widgetRect.mType == CellType.MAIN_WIDGET) {
+            if (widgetRect.getType() == CellType.MAIN_WIDGET) {
                 board.mMain = widgetRect;
             }
-            board.mWidgetsMap.put(widgetRect.mType, widgetRect);
+            board.mWidgetsMap.put(widgetRect.getType(), widgetRect);
         });
         board.mIconPoints = getIconPoints(board.mWidget);
         board.mFolderPoints = getFolderPoints(board.mWidget);
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java
deleted file mode 100644
index 49c146b..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.celllayout.board;
-
-public class CellType {
-    // The cells marked by this will be filled by 1x1 widgets and will be ignored when
-    // validating
-    public static final char IGNORE = 'x';
-    // The cells marked by this will be filled by app icons
-    public static final char ICON = 'i';
-    // The cells marked by FOLDER will be filled by folders with 27 app icons inside
-    public static final char FOLDER = 'Z';
-    // Empty space
-    public static final char EMPTY = '-';
-    // Widget that will be saved as "main widget" for easier retrieval
-    public static final char MAIN_WIDGET = 'm';
-    // Everything else will be consider a widget
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java
deleted file mode 100644
index 39ba434..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.celllayout.board;
-
-import android.graphics.Point;
-
-public class FolderPoint {
-    public Point coord;
-    public char mType;
-
-    public FolderPoint(Point coord, char type) {
-        this.coord = coord;
-        mType = type;
-    }
-
-    /**
-     * [A-Z]: Represents a folder and number of icons in the folder is represented by
-     * the order of letter in the alphabet, A=2, B=3, C=4 ... etc.
-     */
-    public int getNumberIconsInside() {
-        return (mType - 'A') + 2;
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java
deleted file mode 100644
index d3d2970..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.celllayout.board;
-
-import android.graphics.Point;
-
-public class IconPoint {
-    public Point coord;
-    public char mType;
-
-    public IconPoint(Point coord, char type) {
-        this.coord = coord;
-        mType = type;
-    }
-
-    public char getType() {
-        return mType;
-    }
-
-    public void setType(char type) {
-        mType = type;
-    }
-
-    public Point getCoord() {
-        return coord;
-    }
-
-    public void setCoord(Point coord) {
-        this.coord = coord;
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt
index a4a420c..aacd940 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt
@@ -26,11 +26,11 @@
 
     /** Converts a list of WidgetRect into a map of the count of different widget.bounds */
     private fun widgetsToBoundsMap(widgets: List<WidgetRect>) =
-        widgets.groupingBy { it.mBounds }.eachCount()
+        widgets.groupingBy { it.bounds }.eachCount()
 
     /** Converts a list of IconPoint into a map of the count of different icon.coord */
     private fun iconsToPosCountMap(widgets: List<IconPoint>) =
-        widgets.groupingBy { it.getCoord() }.eachCount()
+        widgets.groupingBy { it.coord }.eachCount()
 
     override fun compare(
         cellLayoutBoard: CellLayoutBoard,
@@ -47,7 +47,7 @@
             widgetsToBoundsMap(
                 otherCellLayoutBoard.widgets
                     .filter { !it.shouldIgnore() }
-                    .filter { !overlapsWithIgnored(ignoredRectangles, it.mBounds) }
+                    .filter { !overlapsWithIgnored(ignoredRectangles, it.bounds) }
             )
 
         if (widgetsMap != otherWidgetMap) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java
deleted file mode 100644
index 8a427dd..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.celllayout.board;
-
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.launcher3.ui.TestViewHelpers.findWidgetProvider;
-import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.graphics.Rect;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.Log;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-
-import java.util.function.Supplier;
-import java.util.stream.IntStream;
-
-public class TestWorkspaceBuilder {
-
-    private static final String TAG = "CellLayoutBoardBuilder";
-    private static final String TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests.";
-    private ComponentName mAppComponentName = new ComponentName(
-            "com.google.android.calculator", "com.android.calculator2.Calculator");
-    private UserHandle mMyUser;
-
-    private Context mContext;
-
-    public TestWorkspaceBuilder(Context context) {
-        mMyUser = Process.myUserHandle();
-        mContext = context;
-    }
-
-    /**
-     * Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases.
-     */
-    private FavoriteItemsTransaction fillWithWidgets(WidgetRect widgetRect,
-            FavoriteItemsTransaction transaction, int screenId) {
-        int initX = widgetRect.getCellX();
-        int initY = widgetRect.getCellY();
-        for (int x = initX; x < initX + widgetRect.getSpanX(); x++) {
-            for (int y = initY; y < initY + widgetRect.getSpanY(); y++) {
-                try {
-                    // this widgets are filling, we don't care if we can't place them
-                    transaction.addItem(createWidgetInCell(
-                            new WidgetRect(CellType.IGNORE,
-                                    new Rect(x, y, x, y)), screenId));
-                } catch (Exception e) {
-                    Log.d(TAG, "Unable to place filling widget at " + x + "," + y);
-                }
-            }
-        }
-        return transaction;
-    }
-
-    private AppInfo getApp() {
-        return new AppInfo(mAppComponentName, "test icon", mMyUser,
-                AppInfo.makeLaunchIntent(mAppComponentName));
-    }
-
-    /**
-     * Helper to set the app to use for the test workspace,
-     *  using activity-alias from AndroidManifest-common.
-     * @param testAppName the android:name field of the test app activity-alias to use
-     */
-    public void setTestAppActivityAlias(String testAppName) {
-        this.mAppComponentName = new ComponentName(
-            getInstrumentation().getContext().getPackageName(),
-        TEST_ACTIVITY_PACKAGE_PREFIX + testAppName
-        );
-    }
-
-    private void addCorrespondingWidgetRect(WidgetRect widgetRect,
-            FavoriteItemsTransaction transaction, int screenId) {
-        if (widgetRect.mType == 'x') {
-            fillWithWidgets(widgetRect, transaction, screenId);
-        } else {
-            transaction.addItem(createWidgetInCell(widgetRect, screenId));
-        }
-    }
-
-    /**
-     * Builds the given board into the transaction
-     */
-    public FavoriteItemsTransaction buildFromBoard(CellLayoutBoard board,
-            FavoriteItemsTransaction transaction, final int screenId) {
-        board.getWidgets().forEach(
-                (widgetRect) -> addCorrespondingWidgetRect(widgetRect, transaction, screenId));
-        board.getIcons().forEach((iconPoint) ->
-                transaction.addItem(() -> createIconInCell(iconPoint, screenId))
-        );
-        board.getFolders().forEach((folderPoint) ->
-                transaction.addItem(() -> createFolderInCell(folderPoint, screenId))
-        );
-        return transaction;
-    }
-
-    /**
-     * Fills the hotseat row with apps instead of suggestions, for this to work the workspace should
-     * be clean otherwise this doesn't overrides the existing icons.
-     */
-    public FavoriteItemsTransaction fillHotseatIcons(FavoriteItemsTransaction transaction) {
-        IntStream.range(0, InvariantDeviceProfile.INSTANCE.get(mContext).numDatabaseHotseatIcons)
-                .forEach(i -> transaction.addItem(() -> getHotseatValues(i)));
-        return transaction;
-    }
-
-    private Supplier<ItemInfo> createWidgetInCell(
-            WidgetRect widgetRect, int screenId) {
-        // Create the widget lazily since the appWidgetId can get lost during setup
-        return () -> {
-            LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
-            LauncherAppWidgetInfo item = createWidgetInfo(info, getApplicationContext(), true);
-            item.cellX = widgetRect.getCellX();
-            item.cellY = widgetRect.getCellY();
-            item.spanX = widgetRect.getSpanX();
-            item.spanY = widgetRect.getSpanY();
-            item.screenId = screenId;
-            return item;
-        };
-    }
-
-    public FolderInfo createFolderInCell(FolderPoint folderPoint, int screenId) {
-        FolderInfo folderInfo = new FolderInfo();
-        folderInfo.screenId = screenId;
-        folderInfo.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-        folderInfo.cellX = folderPoint.coord.x;
-        folderInfo.cellY = folderPoint.coord.y;
-        folderInfo.minSpanY = folderInfo.minSpanX = folderInfo.spanX = folderInfo.spanY = 1;
-        folderInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null);
-
-        for (int i = 0; i < folderPoint.getNumberIconsInside(); i++) {
-            folderInfo.add(getDefaultWorkspaceItem(screenId), false);
-        }
-
-        return folderInfo;
-    }
-
-    private WorkspaceItemInfo getDefaultWorkspaceItem(int screenId) {
-        WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.screenId = screenId;
-        item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
-        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-        return item;
-    }
-
-    private ItemInfo createIconInCell(IconPoint iconPoint, int screenId) {
-        WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.screenId = screenId;
-        item.cellX = iconPoint.getCoord().x;
-        item.cellY = iconPoint.getCoord().y;
-        item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
-        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-        return item;
-    }
-
-    private ItemInfo getHotseatValues(int x) {
-        WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.cellX = x;
-        item.cellY = 0;
-        item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
-        item.rank = x;
-        item.screenId = x;
-        item.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-        return item;
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
new file mode 100644
index 0000000..8952b85
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.celllayout.board
+
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.Rect
+import android.os.Process
+import android.os.UserHandle
+import android.util.Log
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.celllayout.FavoriteItemsTransaction
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.ui.TestViewHelpers
+import com.android.launcher3.util.WidgetUtils
+import java.util.function.Supplier
+
+class TestWorkspaceBuilder(private val mContext: Context) {
+
+    private var appComponentName =
+        ComponentName("com.google.android.calculator", "com.android.calculator2.Calculator")
+    private val myUser: UserHandle = Process.myUserHandle()
+
+    /** Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases. */
+    private fun fillWithWidgets(
+        widgetRect: WidgetRect,
+        transaction: FavoriteItemsTransaction,
+        screenId: Int
+    ): FavoriteItemsTransaction {
+        val initX = widgetRect.cellX
+        val initY = widgetRect.cellY
+        for (x in initX until initX + widgetRect.spanX) {
+            for (y in initY until initY + widgetRect.spanY) {
+                try {
+                    // this widgets are filling, we don't care if we can't place them
+                    transaction.addItem(
+                        createWidgetInCell(WidgetRect(CellType.IGNORE, Rect(x, y, x, y)), screenId)
+                    )
+                } catch (e: Exception) {
+                    Log.d(TAG, "Unable to place filling widget at $x,$y")
+                }
+            }
+        }
+        return transaction
+    }
+
+    private fun app() =
+        AppInfo(appComponentName, "test icon", myUser, AppInfo.makeLaunchIntent(appComponentName))
+
+    /**
+     * Helper to set the app to use for the test workspace, using activity-alias from
+     * AndroidManifest-common.
+     *
+     * @param testAppName the android:name field of the test app activity-alias to use
+     */
+    fun setTestAppActivityAlias(testAppName: String) {
+        appComponentName =
+            ComponentName(
+                InstrumentationRegistry.getInstrumentation().context.packageName,
+                TEST_ACTIVITY_PACKAGE_PREFIX + testAppName
+            )
+    }
+
+    private fun addCorrespondingWidgetRect(
+        widgetRect: WidgetRect,
+        transaction: FavoriteItemsTransaction,
+        screenId: Int
+    ) {
+        if (widgetRect.type == 'x') {
+            fillWithWidgets(widgetRect, transaction, screenId)
+        } else {
+            transaction.addItem(createWidgetInCell(widgetRect, screenId))
+        }
+    }
+
+    /** Builds the given board into the transaction */
+    fun buildFromBoard(
+        board: CellLayoutBoard,
+        transaction: FavoriteItemsTransaction,
+        screenId: Int
+    ): FavoriteItemsTransaction {
+        board.widgets.forEach { addCorrespondingWidgetRect(it, transaction, screenId) }
+        board.icons.forEach { transaction.addItem { createIconInCell(it, screenId) } }
+        board.folders.forEach { transaction.addItem { createFolderInCell(it, screenId) } }
+        return transaction
+    }
+
+    /**
+     * Fills the hotseat row with apps instead of suggestions, for this to work the workspace should
+     * be clean otherwise this doesn't overrides the existing icons.
+     */
+    fun fillHotseatIcons(transaction: FavoriteItemsTransaction): FavoriteItemsTransaction {
+        for (i in 0..<InvariantDeviceProfile.INSTANCE[mContext].numDatabaseHotseatIcons) {
+            transaction.addItem { getHotseatValues(i) }
+        }
+        return transaction
+    }
+
+    private fun createWidgetInCell(widgetRect: WidgetRect, paramScreenId: Int): Supplier<ItemInfo> {
+        // Create the widget lazily since the appWidgetId can get lost during setup
+        return Supplier<ItemInfo> {
+            WidgetUtils.createWidgetInfo(
+                    TestViewHelpers.findWidgetProvider(false),
+                    ApplicationProvider.getApplicationContext(),
+                    true
+                )
+                .apply {
+                    cellX = widgetRect.cellX
+                    cellY = widgetRect.cellY
+                    spanX = widgetRect.spanX
+                    spanY = widgetRect.spanY
+                    screenId = paramScreenId
+                }
+        }
+    }
+
+    fun createFolderInCell(folderPoint: FolderPoint, paramScreenId: Int): FolderInfo =
+        FolderInfo().apply {
+            screenId = paramScreenId
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+            cellX = folderPoint.coord.x
+            cellY = folderPoint.coord.y
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null)
+            for (i in 0 until folderPoint.numberIconsInside) {
+                add(getDefaultWorkspaceItem(paramScreenId), false)
+            }
+        }
+
+    private fun getDefaultWorkspaceItem(paramScreenId: Int): WorkspaceItemInfo =
+        WorkspaceItemInfo(app()).apply {
+            screenId = paramScreenId
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+        }
+
+    private fun createIconInCell(iconPoint: IconPoint, paramScreenId: Int) =
+        WorkspaceItemInfo(app()).apply {
+            screenId = paramScreenId
+            cellX = iconPoint.coord.x
+            cellY = iconPoint.coord.y
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+        }
+
+    private fun getHotseatValues(x: Int) =
+        WorkspaceItemInfo(app()).apply {
+            cellX = x
+            cellY = 0
+            spanY = 1
+            spanX = 1
+            minSpanX = 1
+            minSpanY = 1
+            rank = x
+            screenId = x
+            container = LauncherSettings.Favorites.CONTAINER_HOTSEAT
+        }
+
+    companion object {
+        private const val TAG = "CellLayoutBoardBuilder"
+        private const val TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests."
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java
deleted file mode 100644
index c90ce85..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.celllayout.board;
-
-import android.graphics.Rect;
-
-public class WidgetRect {
-    public char mType;
-    public Rect mBounds;
-
-    public WidgetRect(char type, Rect bounds) {
-        this.mType = type;
-        this.mBounds = bounds;
-    }
-
-    public int getSpanX() {
-        return mBounds.right - mBounds.left + 1;
-    }
-
-    public int getSpanY() {
-        return mBounds.top - mBounds.bottom + 1;
-    }
-
-    public int getCellX() {
-        return mBounds.left;
-    }
-
-    public int getCellY() {
-        return mBounds.bottom;
-    }
-
-    boolean shouldIgnore() {
-        return this.mType == CellType.IGNORE;
-    }
-
-    boolean contains(int x, int y) {
-        return mBounds.contains(x, y);
-    }
-
-    @Override
-    public String toString() {
-        return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY()
-                + " xs = " + getSpanX() + " ys = " + getSpanY();
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
index 71f7d47..ff545fe 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -186,6 +186,35 @@
         assertThat(underTest.widgetsByComponentKey).isEmpty()
     }
 
+    @Test
+    fun getWidgetsByPackageItem_returnsACopyOfMap() {
+        loadWidgets()
+
+        val latch = CountDownLatch(1)
+        Executors.MODEL_EXECUTOR.execute {
+            var update = true
+
+            // each "widgetsByPackageItem" read returns a different copy of the map held internally.
+            // Modifying one shouldn't impact another.
+            for ((_, _) in underTest.widgetsByPackageItem.entries) {
+                underTest.widgetsByPackageItem.clear()
+                if (update) { // trigger update
+                    update = false
+                    // Similarly, model could update its code independently while a client is
+                    // iterating on the list.
+                    underTest.update(app, /* packageUser= */ null)
+                }
+            }
+
+            latch.countDown()
+        }
+        if (!latch.await(LOAD_WIDGETS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+            fail("Timed out waiting for test")
+        }
+
+        // No exception
+    }
+
     private fun loadWidgets() {
         val latch = CountDownLatch(1)
         Executors.MODEL_EXECUTOR.execute {
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
deleted file mode 100644
index 8204313..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
+++ /dev/null
@@ -1,116 +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.recyclerview
-
-import android.content.Context
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.launcher3.util.Executors
-import com.android.launcher3.views.ActivityContext
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
-
-    private lateinit var underTest: AllAppsRecyclerViewPool<T>
-    private lateinit var adapter: RecyclerView.Adapter<*>
-
-    @Mock private lateinit var parent: ViewGroup
-    @Mock private lateinit var itemView: View
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        underTest = spy(AllAppsRecyclerViewPool())
-        adapter =
-            object : RecyclerView.Adapter<ViewHolder>() {
-                override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
-                    object : ViewHolder(itemView) {}
-
-                override fun getItemCount() = 0
-
-                override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
-            }
-        underTest.setMaxRecycledViews(VIEW_TYPE, 20)
-    }
-
-    @Test
-    fun preinflate_success() {
-        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
-
-        awaitTasksCompleted()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
-    }
-
-    @Test
-    fun preinflate_not_triggered() {
-        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 0 }
-
-        awaitTasksCompleted()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
-    }
-
-    @Test
-    fun preinflate_cancel_before_runOnMainThread() {
-        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
-        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
-
-        underTest.clear()
-
-        awaitTasksCompleted()
-        verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
-        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
-    }
-
-    @Test
-    fun preinflate_cancel_after_run() {
-        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
-        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
-        awaitTasksCompleted()
-
-        underTest.clear()
-
-        verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
-        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
-    }
-
-    private fun awaitTasksCompleted() {
-        Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
-        Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
-    }
-
-    companion object {
-        private const val VIEW_TYPE: Int = 4
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index a1f2d50..aa7f388 100644
--- a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -26,7 +26,7 @@
 import static com.android.launcher3.BubbleTextView.DISPLAY_PREDICTION_ROW;
 import static com.android.launcher3.BubbleTextView.DISPLAY_SEARCH_RESULT;
 import static com.android.launcher3.BubbleTextView.DISPLAY_SEARCH_RESULT_SMALL;
-import static com.android.launcher3.Flags.FLAG_ENABLE_NEW_ARCHIVING_ICON;
+import static com.android.launcher3.Flags.FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS;
 import static com.android.launcher3.LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 
@@ -416,13 +416,14 @@
         assertThat(mBubbleTextView.getIcon().hasBadge()).isEqualTo(false);
     }
 
-    @EnableFlags(FLAG_ENABLE_NEW_ARCHIVING_ICON)
+    @EnableFlags(FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS)
     @Test
     public void applyIconAndLabel_setsImageSpan_whenInactiveArchivedApp() {
         // Given
         BubbleTextView spyTextView = spy(mBubbleTextView);
         mGmailAppInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
         BubbleTextView expectedTextView = new BubbleTextView(mContext);
+        mContext.getResources().getConfiguration().fontWeightAdjustment = 0;
         int expectedDrawableId = mContext.getResources().getIdentifier(
                 "cloud_download_24px", /* name */
                 "drawable", /* defType */
@@ -451,7 +452,7 @@
         assertThat(actualSpan.getVerticalAlignment()).isEqualTo(ALIGN_CENTER);
     }
 
-    @EnableFlags(FLAG_ENABLE_NEW_ARCHIVING_ICON)
+    @EnableFlags(FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS)
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
     @Test
     public void applyIconAndLabel_setsBoldDrawable_whenBoldedTextForArchivedApp() {
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt
new file mode 100644
index 0000000..79b493a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.Executors
+import java.util.function.IntConsumer
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LauncherAppWidgetHostTest {
+
+    @Mock private lateinit var onAppWidgetRemovedCallback: IntConsumer
+
+    private val context = ActivityContextWrapper(getInstrumentation().targetContext)
+    private lateinit var underTest: LauncherAppWidgetHost
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = LauncherAppWidgetHost(context, onAppWidgetRemovedCallback, emptyList())
+    }
+
+    @Test
+    fun `Host set view to recycle`() {
+        val mockRecycleView = mock(ListenableHostView::class.java)
+
+        assertNull(underTest.viewToRecycle)
+        underTest.recycleViewForNextCreation(mockRecycleView)
+
+        assertSame(mockRecycleView, underTest.viewToRecycle)
+    }
+
+    @Test
+    fun `Host create view`() {
+        val mockRecycleView = mock(ListenableHostView::class.java)
+
+        var resultView = underTest.onCreateView(context, WIDGET_ID, null)
+
+        assertNotSame(mockRecycleView, resultView)
+
+        underTest.recycleViewForNextCreation(mockRecycleView)
+        resultView = underTest.onCreateView(context, WIDGET_ID, null)
+
+        assertSame(mockRecycleView, resultView)
+    }
+
+    @Test
+    fun `Runnable called when app widget removed`() {
+        underTest.onAppWidgetRemoved(WIDGET_ID)
+
+        Executors.MODEL_EXECUTOR.submit {}.get()
+        getInstrumentation().waitForIdleSync()
+
+        verify(onAppWidgetRemovedCallback).accept(WIDGET_ID)
+    }
+
+    companion object {
+        const val WIDGET_ID = 10001
+    }
+}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
index de48432..398f9c5 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
@@ -86,6 +86,8 @@
     private static final String CAMERA_PACKAGE_NAME = "com.android.launcher3.tests.camera";
     private static final int CONTAINER_HEADER_ELEMENT_COUNT = 1;
     private static final int LOCK_UNLOCK_BUTTON_COUNT = 1;
+    private static final int MAIN_USER_APP_COUNT = 1;
+    private static final int VIEW_AT_END_OF_APP_LIST = 1;
     private static final int PS_SETTINGS_BUTTON_COUNT_VISIBLE = 1;
     private static final int PS_SETTINGS_BUTTON_COUNT_INVISIBLE = 0;
     private static final int PS_TRANSITION_IMAGE_COUNT = 1;
@@ -300,8 +302,8 @@
         int rows = (int) (ALL_APPS_HEIGHT - PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT);
         int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
 
-        // The number of adapterItems should be the private space apps + one main app + header.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT
+                        + CONTAINER_HEADER_ELEMENT_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(position,
                 privateProfileManager.scrollForHeaderToBeVisibleInContainer(
@@ -335,8 +337,8 @@
         int rows = (int) (ALL_APPS_HEIGHT - PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT) - 1;
         int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
 
-        // The number of adapterItems should be the private space apps + one main app + header.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT
+                        + CONTAINER_HEADER_ELEMENT_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(position,
                 privateProfileManager.scrollForHeaderToBeVisibleInContainer(
@@ -370,8 +372,8 @@
         int rows = (int) (ALL_APPS_HEIGHT - BIGGER_PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT);
         int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
 
-        // The number of adapterItems should be the private space apps + one main app + header.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT
+                        + CONTAINER_HEADER_ELEMENT_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(position,
                 privateProfileManager.scrollForHeaderToBeVisibleInContainer(
@@ -399,8 +401,7 @@
         mAlphabeticalAppsList.updateItemFilter(info -> info != null
                 && info.user.equals(MAIN_HANDLE));
 
-        // The number of adapterItems should be the private space apps + one main app.
-        assertEquals(NUM_PRIVATE_SPACE_APPS + 1,
+        assertEquals(NUM_PRIVATE_SPACE_APPS + MAIN_USER_APP_COUNT + VIEW_AT_END_OF_APP_LIST,
                 mAlphabeticalAppsList.getAdapterItems().size());
         assertEquals(SCROLL_NO_WHERE, privateProfileManager.scrollForHeaderToBeVisibleInContainer(
                 new AllAppsRecyclerView(mContext),
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
index 58b915f..7182cf3 100644
--- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -94,7 +94,13 @@
 
         val srcCountMap = itemsToMap(srcGrid.items)
         val resultCountMap = itemsToMap(resultItems)
-        val diff = resultCountMap - srcCountMap
+        val diff = resultCountMap.toMutableMap()
+        for ((srcKey, srcValue) in srcCountMap) {
+            val destValue = diff[srcKey]
+            if (destValue != null) {
+                diff[srcKey] = destValue - srcValue
+            }
+        }
 
         diff.forEach { (k, count) ->
             assert(count >= 0) { "Source item $k not present on the result" }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index c926ba9..749a75a 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
 import static com.android.launcher3.testing.shared.TestProtocol.WIDGET_CONFIG_NULL_EXTRA_INTENT;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -44,6 +43,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.rule.LimitDevicesRule;
 import android.system.OsConstants;
 import android.util.Log;
 
@@ -64,7 +64,6 @@
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule;
@@ -224,6 +223,9 @@
     @Rule
     public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
 
+    @Rule
+    public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
+
     public static void initialize(AbstractLauncherUiTest test) throws Exception {
         test.reinitializeLauncherData();
         test.mDevice.pressHome();
@@ -237,16 +239,12 @@
     }
 
     protected void clearPackageData(String pkg) throws IOException, InterruptedException {
-        final CountDownLatch count = new CountDownLatch(2);
-        final SimpleBroadcastReceiver broadcastReceiver =
-                new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> count.countDown());
-        // We OK to make binder calls on main thread in test.
-        broadcastReceiver.registerPkgActions(mTargetContext, pkg,
-                Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED);
-
-        mDevice.executeShellCommand("pm clear " + pkg);
-        assertTrue(pkg + " didn't restart", count.await(20, TimeUnit.SECONDS));
-        mTargetContext.unregisterReceiver(broadcastReceiver);
+        assertTrue("pm clear command failed",
+                mDevice.executeShellCommand("pm clear " + pkg)
+                .contains("Success"));
+        assertTrue("pm wait-for-handler command failed",
+                mDevice.executeShellCommand("pm wait-for-handler")
+                .contains("Success"));
     }
 
     protected TestRule getRulesInsideActivityMonitor() {
diff --git a/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
new file mode 100644
index 0000000..1cee71c
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.ui
+
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.views.DoubleShadowIconDrawable
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DoubleShadowIconDrawableTest {
+
+    @Test
+    fun `DoubleShadowIconDrawable is setup correctly from given ShadowInfo`() {
+        // Given
+        val shadowInfo: ShadowInfo = mock()
+        val originalDrawable: Drawable = mock()
+        val iconSize = 2
+        val iconInsetSize = 1
+        // When
+        val drawableUnderTest =
+            DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize)
+        // Then
+        assertThat(drawableUnderTest.intrinsicHeight).isEqualTo(iconSize)
+        assertThat(drawableUnderTest.intrinsicWidth).isEqualTo(iconSize)
+    }
+
+    @Test
+    fun `createShadowRenderNode creates RenderNode for shadow effects`() {
+        // Given
+        val shadowInfo =
+            ShadowInfo(
+                ambientShadowBlur = 1f,
+                ambientShadowColor = 2,
+                keyShadowBlur = 3f,
+                keyShadowOffsetX = 4f,
+                keyShadowOffsetY = 5f,
+                keyShadowColor = 6
+            )
+        val originalDrawable: Drawable = mock()
+        val iconSize = 2
+        val iconInsetSize = 1
+        // When
+        val shadowDrawableUnderTest =
+            spy(DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize))
+        shadowDrawableUnderTest.createShadowRenderNode()
+        // Then
+        verify(shadowDrawableUnderTest)
+            .createShadowRenderEffect(
+                shadowInfo.ambientShadowBlur,
+                0f,
+                0f,
+                Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+            )
+        verify(shadowDrawableUnderTest)
+            .createShadowRenderEffect(
+                shadowInfo.keyShadowBlur,
+                shadowInfo.keyShadowOffsetX,
+                shadowInfo.keyShadowOffsetY,
+                Color.alpha(shadowInfo.keyShadowColor).toFloat()
+            )
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
new file mode 100644
index 0000000..ef4dc1a
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.ui
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadowInfoTest {
+
+    @Test
+    fun `ShadowInfo is created correctly from context`() {
+        // Given
+        val mockContext: Context = mock()
+        val mockAttrs: AttributeSet = mock()
+        val styledAttrs: TypedArray = mock()
+        val expectedShadowInfo =
+            ShadowInfo(
+                ambientShadowBlur = 1f,
+                ambientShadowColor = 2,
+                keyShadowBlur = 3f,
+                keyShadowOffsetX = 4f,
+                keyShadowOffsetY = 5f,
+                keyShadowColor = 6
+            )
+        doReturn(styledAttrs)
+            .whenever(mockContext)
+            .obtainStyledAttributes(mockAttrs, R.styleable.ShadowInfo, 0, 0)
+        doReturn(1)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+        doReturn(2).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_ambientShadowColor, 0)
+        doReturn(3)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+        doReturn(4)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+        doReturn(5)
+            .whenever(styledAttrs)
+            .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+        doReturn(6).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+        // When
+        val actualShadowInfo = ShadowInfo.fromContext(mockContext, mockAttrs, 0)
+        // Then
+        assertThat(actualShadowInfo.ambientShadowBlur).isEqualTo(1)
+        assertThat(actualShadowInfo.ambientShadowColor).isEqualTo(2)
+        assertThat(actualShadowInfo.keyShadowBlur).isEqualTo(3)
+        assertThat(actualShadowInfo.keyShadowOffsetX).isEqualTo(4)
+        assertThat(actualShadowInfo.keyShadowOffsetY).isEqualTo(5)
+        assertThat(actualShadowInfo.keyShadowColor).isEqualTo(6)
+        assertThat(actualShadowInfo).isEqualTo(expectedShadowInfo)
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 988aa94..b7ebfcd 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.tapl.BaseOverview.TASK_RES_ID;
+import static com.android.launcher3.tapl.BaseOverview.TASK_SELECTOR;
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
 
@@ -117,10 +117,10 @@
                         // non-tablet overview, snapshots can be on either side of the swiped
                         // task, but we still check that they become visible after swiping and
                         // pausing.
-                        mLauncher.waitForOverviewObject(TASK_RES_ID);
+                        mLauncher.waitForObjectBySelector(TASK_SELECTOR);
                         if (mLauncher.isTablet()) {
                             List<UiObject2> tasks = mLauncher.getDevice().findObjects(
-                                    mLauncher.getOverviewObjectSelector(TASK_RES_ID));
+                                    TASK_SELECTOR);
                             final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
                             mLauncher.assertTrue(
                                     "All tasks not to the left of the swiped task",
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 567a8bd..e71b49f 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -19,7 +19,9 @@
 import static android.view.KeyEvent.KEYCODE_ESCAPE;
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.tapl.LauncherInstrumentation.log;
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
+import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.graphics.Rect;
@@ -35,9 +37,11 @@
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -46,7 +50,9 @@
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
     private static final String TAG = "BaseOverview";
-    protected static final String TASK_RES_ID = "task";
+    protected static final BySelector TASK_SELECTOR = By.res(Pattern.compile(
+            getOverviewPackageName()
+                    + ":id/(task_view_single|task_view_grouped|task_view_desktop)"));
     private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
     private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
@@ -56,10 +62,22 @@
 
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
 
+    private final @Nullable UiObject2 mLiveTileTask;
+
+
     BaseOverview(LauncherInstrumentation launcher) {
+        this(launcher, /*launchedFromApp=*/false);
+    }
+
+    BaseOverview(LauncherInstrumentation launcher, boolean launchedFromApp) {
         super(launcher);
         verifyActiveContainer();
         verifyActionsViewVisibility();
+        if (launchedFromApp) {
+            mLiveTileTask = getCurrentTaskUnchecked();
+        } else {
+            mLiveTileTask = null;
+        }
     }
 
     @Override
@@ -79,7 +97,7 @@
     private void flingForwardImpl() {
         try (LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling forward in overview")) {
-            LauncherInstrumentation.log("Overview.flingForward before fling");
+            log("Overview.flingForward before fling");
             final UiObject2 overview = verifyActiveContainer();
             final int leftMargin =
                     mLauncher.getTargetInsets().left + mLauncher.getEdgeSensitivityWidth();
@@ -105,7 +123,7 @@
     private void flingBackwardImpl() {
         try (LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling backward in overview")) {
-            LauncherInstrumentation.log("Overview.flingBackward before fling");
+            log("Overview.flingBackward before fling");
             final UiObject2 overview = verifyActiveContainer();
             final int rightMargin =
                     mLauncher.getTargetInsets().right + mLauncher.getEdgeSensitivityWidth();
@@ -276,37 +294,56 @@
      */
     @NonNull
     public OverviewTask getCurrentTask() {
+        UiObject2 currentTask = getCurrentTaskUnchecked();
+        mLauncher.assertNotNull("Unable to find a task", currentTask);
+        return new OverviewTask(mLauncher, currentTask, this);
+    }
+
+    @Nullable
+    private UiObject2 getCurrentTaskUnchecked() {
         final List<UiObject2> taskViews = getTasks();
-        mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
+        if (taskViews.isEmpty()) {
+            return null;
+        }
 
         // The widest, and most top-right task should be the current task
-        UiObject2 currentTask = Collections.max(taskViews,
+        return Collections.max(taskViews,
                 Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width())
                         .thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x)
                         .thenComparing(Comparator.comparing(
                                 (UiObject2 t) -> t.getVisibleCenter().y).reversed()));
-        return new OverviewTask(mLauncher, currentTask, this);
     }
 
-    /** Returns an overview task matching TestActivity {@param activityNumber}. */
+    /**
+     * Returns an overview task that contains the specified test activity in its thumbnails.
+     *
+     * @param activityIndex index of TestActivity to match against
+     */
     @NonNull
-    public OverviewTask getTestActivityTask(int activityNumber) {
+    public OverviewTask getTestActivityTask(int activityIndex) {
+        return getTestActivityTask(Collections.singleton(activityIndex));
+    }
+
+    /**
+     * Returns an overview task that contains all the specified test activities in its thumbnails.
+     *
+     * @param activityNumbers collection of indices of TestActivity to match against
+     */
+    @NonNull
+    public OverviewTask getTestActivityTask(Collection<Integer> activityNumbers) {
         final List<UiObject2> taskViews = getTasks();
         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
 
-        final String activityName = "TestActivity" + activityNumber;
-        UiObject2 task = null;
-        for (UiObject2 taskView : taskViews) {
-            // TODO(b/239452415): Use equals instead of descEndsWith
-            if (taskView.getParent().hasObject(By.descEndsWith(activityName))) {
-                task = taskView;
-                break;
-            }
-        }
-        mLauncher.assertNotNull(
-                "Unable to find a task with " + activityName + " from the task list", task);
+        Optional<UiObject2> task = taskViews.stream().filter(
+                taskView -> activityNumbers.stream().allMatch(activityNumber ->
+                    // TODO(b/239452415): Use equals instead of descEndsWith
+                    taskView.hasObject(By.descEndsWith("TestActivity" + activityNumber))
+                )).findFirst();
 
-        return new OverviewTask(mLauncher, task, this);
+        mLauncher.assertTrue("Unable to find a task with test activities " + activityNumbers
+                + " from the task list", task.isPresent());
+
+        return new OverviewTask(mLauncher, task.get(), this);
     }
 
     /**
@@ -328,8 +365,7 @@
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get overview tasks")) {
             verifyActiveContainer();
-            return mLauncher.getDevice().findObjects(
-                    mLauncher.getOverviewObjectSelector(TASK_RES_ID));
+            return mLauncher.getDevice().findObjects(TASK_SELECTOR);
         }
     }
 
@@ -506,4 +542,10 @@
         }
         return null;
     }
+
+    protected boolean isLiveTile(UiObject2 task) {
+        // UiObject2.equals returns false even when mLiveTileTask and task have the same node, hence
+        // compare only hashCode as a workaround.
+        return mLiveTileTask != null && mLiveTileTask.hashCode() == task.hashCode();
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 200f2ff..b3ad930 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -33,6 +33,7 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.NonNull;
 import androidx.test.uiautomator.Condition;
 import androidx.test.uiautomator.UiDevice;
 
@@ -75,6 +76,20 @@
         return false;
     }
 
+    @NonNull
+    @Override
+    public BaseOverview switchToOverview() {
+        try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
+                     "want to switch from background to overview")) {
+            verifyActiveContainer();
+            goToOverviewUnchecked();
+            return mLauncher.is3PLauncher()
+                    ? new BaseOverview(mLauncher, /*launchedFromApp=*/true)
+                    : new Overview(mLauncher, /*launchedFromApp=*/true);
+        }
+    }
+
     /**
      * Returns the taskbar.
      *
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 75c1b24..a874062 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1585,7 +1585,7 @@
         return objects;
     }
 
-    private UiObject2 waitForObjectBySelector(BySelector selector) {
+    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);
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 50c2136..deb27e4 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -22,9 +22,12 @@
  * Overview pane.
  */
 public class Overview extends BaseOverview {
-
     Overview(LauncherInstrumentation launcher) {
-        super(launcher);
+        this(launcher, /*launchedFromApp=*/false);
+    }
+
+    Overview(LauncherInstrumentation launcher, boolean launchedFromApp) {
+        super(launcher, launchedFromApp);
     }
 
     @Override
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 6f420af..7a8ab49 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -40,16 +40,23 @@
 public final class OverviewTask {
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
     static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync");
+    static final Pattern TASK_START_EVENT_DESKTOP = Pattern.compile("launchDesktopFromRecents");
+    static final Pattern TASK_START_EVENT_LIVE_TILE = Pattern.compile(
+            "composeRecentsLaunchAnimator");
     static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect");
     static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks");
     private final LauncherInstrumentation mLauncher;
+    @NonNull
     private final UiObject2 mTask;
+    private final TaskViewType mType;
     private final BaseOverview mOverview;
 
-    OverviewTask(LauncherInstrumentation launcher, UiObject2 task, BaseOverview overview) {
+    OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview) {
         mLauncher = launcher;
+        mLauncher.assertNotNull("task must not be null", task);
         mTask = task;
         mOverview = overview;
+        mType = getType();
         verifyActiveContainer();
     }
 
@@ -220,7 +227,22 @@
                     return new LaunchedAppState(mLauncher);
                 }
             } else {
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+                final Pattern event;
+                if (mOverview.isLiveTile(mTask)) {
+                    event = TASK_START_EVENT_LIVE_TILE;
+                } else if (mType == TaskViewType.DESKTOP) {
+                    event = TASK_START_EVENT_DESKTOP;
+                } else {
+                    event = TASK_START_EVENT;
+                }
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, event);
+
+                if (mType == TaskViewType.DESKTOP) {
+                    try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer(
+                            "launched desktop")) {
+                        mLauncher.waitForSystemUiObject("desktop_mode_caption");
+                    }
+                }
                 return new LaunchedAppState(mLauncher);
             }
         }
@@ -273,6 +295,17 @@
         return actual.contains(expected);
     }
 
+    private TaskViewType getType() {
+        String resourceName = mTask.getResourceName();
+        if (resourceName.endsWith("task_view_grouped")) {
+            return TaskViewType.GROUPED;
+        } else if (resourceName.endsWith("task_view_desktop")) {
+            return TaskViewType.DESKTOP;
+        } else {
+            return TaskViewType.SINGLE;
+        }
+    }
+
     /**
      * Enum used to specify  which task is retrieved when it is a split task.
      */
@@ -292,4 +325,10 @@
             this.iconAppRes = iconAppRes;
         }
     }
+
+    private enum TaskViewType {
+        SINGLE,
+        GROUPED,
+        DESKTOP
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index 902ad5b..90d32f3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -97,6 +97,29 @@
         }
     }
 
+    /**
+     * Taps the Desktop item from the overview task menu and returns the LaunchedAppState
+     * representing the Desktop.
+     */
+    @NonNull
+    public LaunchedAppState tapDesktopMenuItem() {
+        try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
+                     "before tapping the desktop menu item")) {
+            mLauncher.executeAndWaitForLauncherStop(
+                    () -> mLauncher.clickLauncherObject(
+                            mLauncher.findObjectInContainer(mMenu, By.text("Desktop"))),
+                    "tapped desktop menu item");
+
+            try (LauncherInstrumentation.Closable ignored2 = mLauncher.addContextLayer(
+                    "tapped desktop menu item")) {
+                mLauncher.waitUntilSystemLauncherObjectGone("overview_panel");
+                mLauncher.waitForSystemUiObject("desktop_mode_caption");
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
     /** Returns true if an item matching the given string is present in the menu. */
     public boolean hasMenuItem(String expectedMenuItemText) {
         UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));
@@ -104,14 +127,6 @@
     }
 
     /**
-     * Returns the menu item specified by name if present.
-     */
-    public OverviewTaskMenuItem getMenuItemByName(String menuItemName) {
-        return new OverviewTaskMenuItem(mLauncher,
-                mLauncher.waitForObjectInContainer(mMenu, By.text(menuItemName)));
-    }
-
-    /**
      * Taps outside task menu to dismiss it.
      */
     public void touchOutsideTaskMenuToDismiss() {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
deleted file mode 100644
index e3035bf..0000000
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.tapl;
-
-import android.graphics.Rect;
-
-import androidx.test.uiautomator.UiObject2;
-
-/** Represents an item in the overview task menu. */
-public class OverviewTaskMenuItem {
-
-    private final LauncherInstrumentation mLauncher;
-    private final UiObject2 mMenuItem;
-
-    OverviewTaskMenuItem(LauncherInstrumentation launcher, UiObject2 menuItem) {
-        mLauncher = launcher;
-        mMenuItem = menuItem;
-    }
-
-    /**
-     * Returns this menu item's visible bounds.
-     */
-    public Rect getVisibleBounds() {
-        return mMenuItem.getVisibleBounds();
-    }
-}