Merge "Improve performance of thumbnail retrieval - hot taskData flow" 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/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/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/drawable/bg_circle.xml b/quickstep/res/drawable/bg_circle.xml
deleted file mode 100644
index 506177b..0000000
--- a/quickstep/res/drawable/bg_circle.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    Copyright (C) 2020 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="oval">
-    <solid android:color="#FFFFFFFF" />
-</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_desktop.xml b/quickstep/res/drawable/ic_desktop.xml
index 8de275d..11feca5 100644
--- a/quickstep/res/drawable/ic_desktop.xml
+++ b/quickstep/res/drawable/ic_desktop.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,17 +14,13 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="32.0dp"
-    android:height="32.0dp"
-    android:viewportWidth="32.0"
-    android:viewportHeight="32.0"
-    >
-    <group android:scaleX="0.5"
-        android:scaleY="0.5"
-        android:translateX="6.0"
-        android:translateY="6.0">
-        <path
-            android:fillColor="@android:color/black"
-            android:pathData="M5.958,37.708Q4.458,37.708 3.354,36.604Q2.25,35.5 2.25,34V18.292Q2.25,16.792 3.354,15.688Q4.458,14.583 5.958,14.583H9.5V5.958Q9.5,4.458 10.625,3.354Q11.75,2.25 13.208,2.25H34Q35.542,2.25 36.646,3.354Q37.75,4.458 37.75,5.958V21.667Q37.75,23.167 36.646,24.271Q35.542,25.375 34,25.375H30.5V34Q30.5,35.5 29.396,36.604Q28.292,37.708 26.792,37.708ZM5.958,34H26.792Q26.792,34 26.792,34Q26.792,34 26.792,34V21.542H5.958V34Q5.958,34 5.958,34Q5.958,34 5.958,34ZM30.5,21.667H34Q34,21.667 34,21.667Q34,21.667 34,21.667V9.208H13.208V14.583H26.833Q28.375,14.583 29.438,15.667Q30.5,16.75 30.5,18.25Z"/>
-    </group>
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M240,640L600,640L600,440L240,440L240,640ZM660,520L720,520L720,320L360,320L360,380L660,380L660,520ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720Z" />
 </vector>
diff --git a/quickstep/res/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/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/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index fe1b403..4a1035f 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -356,14 +356,6 @@
         options.setOnAnimationAbortListener(endCallback);
         options.setOnAnimationFinishedListener(endCallback);
 
-        // Prepare taskbar for animation synchronization. This needs to happen here before any
-        // app transition is created.
-        LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
-        if (enableScalingRevealHomeAnimation() && taskbarController != null) {
-            taskbarController.setIgnoreInAppFlagForSync(true);
-            onEndCallback.add(() -> taskbarController.setIgnoreInAppFlagForSync(false));
-        }
-
         IBinder cookie = mAppLaunchRunner.supportsReturnTransition()
                 ? ((ContainerAnimationRunner) mAppLaunchRunner).getCookie() : null;
         addLaunchCookie(cookie, (ItemInfo) v.getTag(), options);
@@ -1942,21 +1934,6 @@
                 anim.addListener(mForceInvisibleListener);
             }
 
-            // Syncs the app launch animation and taskbar stash animation (if exists).
-            if (enableScalingRevealHomeAnimation()) {
-                LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
-                if (taskbarController != null) {
-                    taskbarController.setIgnoreInAppFlagForSync(false);
-
-                    if (launcherClosing) {
-                        Animator taskbar = taskbarController.createAnimToApp();
-                        if (taskbar != null) {
-                            anim.play(taskbar);
-                        }
-                    }
-                }
-            }
-
             result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy,
                     skipFirstFrame);
         }
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/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 0ba5de1..dbd9c73 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -54,6 +54,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 import java.util.HashMap;
@@ -213,9 +214,16 @@
                         resources.getString(R.string.quick_switch_desktop),
                         Locale.getDefault()).format(args));
             } else {
+                final boolean firstTaskIsLeftTopTask =
+                        groupTask.mSplitBounds == null
+                        || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
+                final Task leftTopTask = firstTaskIsLeftTopTask
+                        ? groupTask.task1 : groupTask.task2;
+                final Task rightBottomTask = firstTaskIsLeftTopTask
+                        ? groupTask.task2 : groupTask.task1;
                 currentTaskView.setThumbnails(
-                        groupTask.task1,
-                        groupTask.task2,
+                        leftTopTask,
+                        rightBottomTask,
                         updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
                         updateTasks ? mViewCallbacks::updateIconInBackground : null);
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 3981e43..8df2eb8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -19,7 +19,6 @@
 import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
 import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
 import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
-import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IGNORE_IN_APP;
 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 
@@ -225,9 +224,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;
         }
 
@@ -276,28 +278,6 @@
         return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
     }
 
-    /**
-     * Create Taskbar animation to be played alongside the Launcher app launch animation.
-     */
-    public @Nullable Animator createAnimToApp() {
-        TaskbarStashController stashController = mControllers.taskbarStashController;
-        stashController.updateStateForFlag(TaskbarStashController.FLAG_IN_APP, true);
-        return stashController.createApplyStateAnimator(stashController.getStashDuration());
-    }
-
-    /**
-     * Temporarily ignore FLAG_IN_APP for app launches to prevent premature taskbar stashing.
-     * This is needed because taskbar gets a signal to stash before we actually start the
-     * app launch animation.
-     */
-    public void setIgnoreInAppFlagForSync(boolean enabled) {
-        if (mControllers == null) {
-            // This method can be called before init() is called.
-            return;
-        }
-        mControllers.taskbarStashController.updateStateForFlag(FLAG_IGNORE_IN_APP, enabled);
-    }
-
     public void updateTaskbarLauncherStateGoingHome() {
         mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, true);
         mTaskbarLauncherStateController.applyState();
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 9d394a8..9c954d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -927,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());
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/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/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 5e7c7ce..4df0223 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -28,6 +28,7 @@
 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;
@@ -86,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;
@@ -97,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..8a20131 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -96,7 +96,6 @@
     public static final int FLAG_STASHED_SYSUI = 1 << 9; //  app pinning,...
     public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ...
     public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview
-    public static final int FLAG_IGNORE_IN_APP = 1 << 12; // used to sync with app launch animation
 
     // If any of these flags are enabled, isInApp should return true.
     private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -1022,6 +1021,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;
 
@@ -1281,11 +1284,6 @@
          */
         @Nullable
         public Animator createSetStateAnimator(long flags, long duration) {
-            // We do this when we want to synchronize the app launch and taskbar stash animations.
-            if (hasAnyFlag(FLAG_IGNORE_IN_APP) && hasAnyFlag(flags, FLAG_IN_APP)) {
-                flags = flags & ~FLAG_IN_APP;
-            }
-
             boolean isStashed = mStashCondition.test(flags);
 
             if (DEBUG) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 2ada5ba..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;
@@ -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 ffa4819..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;
 
@@ -272,6 +274,10 @@
         OneShotPreDrawListener.add(mTaskbarView, listener);
     }
 
+    public Rect getIconLayoutVisualBounds() {
+        return mTaskbarView.getIconLayoutVisualBounds();
+    }
+
     public Rect getIconLayoutBounds() {
         return mTaskbarView.getIconLayoutBounds();
     }
@@ -462,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/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 32ca9f2..819c473 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -124,8 +124,6 @@
 
     private final BubbleBarBackground mBubbleBarBackground;
 
-    private boolean mIsAnimatingNewBubble = false;
-
     /**
      * The current bounds of all the bubble bar. Note that these bounds may not account for
      * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which
@@ -418,6 +416,7 @@
         LayoutParams lp = (LayoutParams) getLayoutParams();
         lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
         setLayoutParams(lp); // triggers a relayout
+        updateBubbleAccessibilityStates();
     }
 
     /**
@@ -660,13 +659,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;
     }
 
     /**
@@ -701,16 +712,6 @@
         return mRelativePivotY;
     }
 
-    /** Notifies the bubble bar that a new bubble animation is starting. */
-    public void onAnimatingBubbleStarted() {
-        mIsAnimatingNewBubble = true;
-    }
-
-    /** Notifies the bubble bar that a new bubble animation is complete. */
-    public void onAnimatingBubbleCompleted() {
-        mIsAnimatingNewBubble = false;
-    }
-
     /** Add a new bubble to the bubble bar. */
     public void addBubble(BubbleView bubble) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
@@ -887,6 +888,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;
@@ -1252,6 +1280,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
@@ -1306,9 +1341,7 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (mIsAnimatingNewBubble) {
-            mController.onBubbleBarTouchedWhileAnimating();
-        }
+        mController.onBubbleBarTouched();
         if (!mIsBarExpanded) {
             // When the bar is collapsed, all taps on it should expand it.
             return true;
@@ -1316,11 +1349,6 @@
         return super.onInterceptTouchEvent(ev);
     }
 
-    /** Whether a new bubble is currently animating. */
-    public boolean isAnimatingNewBubble() {
-        return mIsAnimatingNewBubble;
-    }
-
     private boolean hasOverflow() {
         // Overflow is always the last bubble
         View lastChild = getChildAt(getChildCount() - 1);
@@ -1331,21 +1359,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);
+            }
         }
     }
 
@@ -1435,7 +1481,6 @@
             pw.println("    bubble key: " + key);
         }
         pw.println("  isExpanded: " + isExpanded());
-        pw.println("  mIsAnimatingNewBubble: " + mIsAnimatingNewBubble);
         if (mBubbleAnimator != null) {
             pw.println("  mBubbleAnimator.isRunning(): " + mBubbleAnimator.isRunning());
             pw.println("  mBubbleAnimator is null");
@@ -1460,8 +1505,8 @@
         /** Returns the translation Y that the bubble bar should have. */
         float getBubbleBarTranslationY();
 
-        /** Notifies the controller that the bubble bar was touched while it was animating. */
-        void onBubbleBarTouchedWhileAnimating();
+        /** Notifies the controller that the bubble bar was touched. */
+        void onBubbleBarTouched();
 
         /** Requests the controller to expand bubble bar */
         void expandBubbleBar();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 2cdc0ce..3261262 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -72,6 +72,7 @@
     private BubbleDragController mBubbleDragController;
     private TaskbarStashController mTaskbarStashController;
     private TaskbarInsetsController mTaskbarInsetsController;
+    private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider;
     private View.OnClickListener mBubbleClickListener;
     private View.OnClickListener mBubbleBarClickListener;
     private BubbleView.Controller mBubbleViewController;
@@ -110,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));
@@ -140,8 +144,8 @@
             }
 
             @Override
-            public void onBubbleBarTouchedWhileAnimating() {
-                BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating();
+            public void onBubbleBarTouched() {
+                BubbleBarViewController.this.onBubbleBarTouched();
             }
 
             @Override
@@ -202,9 +206,12 @@
         }
     }
 
-    private void onBubbleBarTouchedWhileAnimating() {
-        mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating();
-        mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
+    private void onBubbleBarTouched() {
+        if (isAnimatingNewBubble()) {
+            mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating();
+            mBubbleStashController.onNewBubbleAnimationInterrupted(false,
+                    mBarView.getTranslationY());
+        }
     }
 
     private void expandBubbleBar() {
@@ -303,7 +310,11 @@
 
     /** Whether a new bubble is animating. */
     public boolean isAnimatingNewBubble() {
-        return mBarView.isAnimatingNewBubble();
+        return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.isAnimating();
+    }
+
+    public boolean isNewBubbleAnimationRunningOrPending() {
+        return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimation();
     }
 
     /** The horizontal margin of the bubble bar from the edge of the screen. */
@@ -347,6 +358,7 @@
             if (hidden) {
                 mBarView.setAlpha(0);
                 mBarView.setExpanded(false);
+                updatePersistentTaskbar(/* isBubbleBarExpanded = */ false);
             }
             mActivity.bubbleBarVisibilityChanged(!hidden);
         }
@@ -575,14 +587,14 @@
         }
         boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome()
                 || !mBubbleStashController.isTransientTaskBar();
-        if (persistentTaskbarOrOnHome && !isExpanding && !isExpanded()) {
-            mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble);
+        if (persistentTaskbarOrOnHome && !isExpanded()) {
+            mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
             return;
         }
 
         // only animate the new bubble if we're in an app, have handle view and not auto expanding
-        if (isInApp && !isExpanding && mBubbleStashController.getHasHandleView() && !isExpanded()) {
-            mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
+        if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) {
+            mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
         }
     }
 
@@ -611,6 +623,7 @@
     public void setExpanded(boolean isExpanded) {
         if (isExpanded != mBarView.isExpanded()) {
             mBarView.setExpanded(isExpanded);
+            updatePersistentTaskbar(isExpanded);
             if (!isExpanded) {
                 mSystemUiProxy.collapseBubbles();
             } else {
@@ -621,11 +634,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 (isNewBubbleAnimationRunningOrPending() && isExpanded) {
+            mBubbleBarViewAnimator.expandedWhileAnimating();
+            return;
+        }
         if (!isExpanded) {
             mBubbleStashController.stashBubbleBar();
         } else {
@@ -750,4 +786,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 8478dc2..e00916a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -15,8 +15,15 @@
  */
 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;
@@ -79,7 +86,20 @@
                 bubbleStashedHandleViewController.orElse(null),
                 taskbarControllers::runAfterInit
         );
-        bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this);
+        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);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 52f5a29..8158fe7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -245,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/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 0a0cfd0..2ed88d8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -43,6 +43,14 @@
     private val bubbleBarBounceDistanceInPx =
         bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
 
+    fun hasAnimation() = animatingBubble != null
+
+    val isAnimating: Boolean
+        get() {
+            val animatingBubble = animatingBubble ?: return false
+            return animatingBubble.state != AnimatingBubble.State.CREATED
+        }
+
     private companion object {
         /** The time to show the flyout. */
         const val FLYOUT_DELAY_MS: Long = 2500
@@ -58,8 +66,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 +130,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,8 +161,8 @@
      *    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
         bubbleBarView.alpha = 0f
         bubbleBarView.translationY = 0f
@@ -138,9 +174,12 @@
         // 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: Float = bubbleStashController.getDiffBetweenHandleAndBarCenters()
-        val stashedHandleTranslationY: Float =
+        val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
+        val stashedHandleTranslationYForAnimation =
             bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
+        val stashedHandleTranslationY =
+            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
@@ -150,15 +189,14 @@
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
         animator.addUpdateListener { handle, values ->
-            val ty: Float =
-                values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
+            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 -> {
@@ -172,8 +210,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 +
@@ -193,18 +231,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()
         }
@@ -227,7 +263,8 @@
      */
     private fun buildBubbleBarToHandleAnimation() = Runnable {
         if (animatingBubble == null) return@Runnable
-        val offset = bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
+        moveToState(AnimatingBubble.State.ANIMATING_OUT)
+        val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
         val stashedHandleTranslationY =
             bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
         // this is the total distance that both the stashed handle and the bar will be traveling
@@ -272,7 +309,6 @@
         animator.addEndListener { _, _, _, canceled, _, _, _ ->
             animatingBubble = null
             if (!canceled) bubbleStashController.stashBubbleBarImmediate()
-            bubbleBarView.onAnimatingBubbleCompleted()
             bubbleBarView.relativePivotY = 1f
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -281,6 +317,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()
@@ -296,36 +334,49 @@
                 Runnable {
                     animatingBubble = null
                     bubbleStashController.showBubbleBarImmediate()
-                    bubbleBarView.onAnimatingBubbleCompleted()
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
             }
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
     private fun buildBubbleBarSpringInAnimation() = Runnable {
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
         // prepare the bubble bar for the animation
-        bubbleBarView.onAnimatingBubbleStarted()
         bubbleBarView.translationY = bubbleBarView.height.toFloat()
         bubbleBarView.visibility = VISIBLE
         bubbleBarView.alpha = 1f
         bubbleBarView.scaleX = 1f
         bubbleBarView.scaleY = 1f
 
+        val translationTracker = TranslationTracker(bubbleBarView.translationY)
+
         val animator = PhysicsAnimator.getInstance(bubbleBarView)
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
-        animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() }
+        animator.addUpdateListener { _, values ->
+            val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
+            translationTracker.updateTyAndExpandIfNeeded(ty)
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
         animator.addEndListener { _, _, _, _, _, _, _ ->
+            if (animatingBubble?.expand == true) {
+                cancelHideAnimation()
+            } else {
+                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()
@@ -333,10 +384,10 @@
         val hideAnimation = Runnable {
             animatingBubble = null
             bubbleStashController.showBubbleBarImmediate()
-            bubbleBarView.onAnimatingBubbleCompleted()
             bubbleStashController.updateTaskbarTouchRegion()
         }
-        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        animatingBubble =
+            AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
@@ -347,17 +398,28 @@
      * the bubble bar moves back to its initial position with a spring animation.
      */
     private fun buildBubbleBarBounceAnimation() = Runnable {
-        bubbleBarView.onAnimatingBubbleStarted()
+        moveToState(AnimatingBubble.State.ANIMATING_IN)
         val ty = bubbleStashController.bubbleBarTranslationY
 
         val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
         springBackAnimation.setDefaultSpringConfig(springConfig)
         springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
+        springBackAnimation.addEndListener { _, _, _, _, _, _, _ ->
+            if (animatingBubble?.expand == true) {
+                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()
     }
 
@@ -367,7 +429,6 @@
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
-        bubbleBarView.onAnimatingBubbleCompleted()
         bubbleBarView.relativePivotY = 1f
         animatingBubble = null
     }
@@ -378,7 +439,6 @@
         scheduler.cancel(hideAnimation)
         animatingBubble = null
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
-        bubbleBarView.onAnimatingBubbleCompleted()
         bubbleBarView.relativePivotY = 1f
         bubbleStashController.onNewBubbleAnimationInterrupted(
             /* isStashed= */ bubbleBarView.alpha == 0f,
@@ -386,6 +446,24 @@
         )
     }
 
+    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.relativePivotY = 1f
+        bubbleStashController.showBubbleBarImmediate()
+    }
+
     private fun <T> PhysicsAnimator<T>?.cancelIfRunning() {
         if (this?.isRunning() == true) cancel()
     }
@@ -405,4 +483,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 0f43744..48eb7de 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -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/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/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b2cc369..c3d3729 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;
@@ -75,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;
@@ -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 =
@@ -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();
     }
 
@@ -1382,10 +1388,11 @@
      */
     public void launchSplitTasks(
             @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
-        // Top/left and bottom/right tasks respectively.
-        Task task1 = groupTask.task1;
+        // SplitBounds can be null if coming from Taskbar launch.
+        final boolean firstTaskIsLeftTopTask = isFirstTaskLeftTopTask(groupTask);
         // task2 should never be null when calling this method. Allow a crash to catch invalid calls
-        Task task2 = groupTask.task2;
+        Task task1 = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
+        Task task2 = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
         mSplitSelectStateController.launchExistingSplitPair(
                 null /* launchingTaskView */,
                 task1.key.id,
@@ -1399,12 +1406,23 @@
                 remoteTransition);
     }
 
+    private static boolean isFirstTaskLeftTopTask(@NonNull GroupTask groupTask) {
+        return groupTask.mSplitBounds == null
+                || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
+    }
+
     /**
      * Launches two apps as an app pair.
      */
     public void launchAppPair(AppPairIcon appPairIcon) {
+        // Potentially show the Taskbar education once the app pair launch finishes
         mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon,
-                CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE);
+                CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
+                (success) -> {
+                    if (success && mTaskbarUIController != null) {
+                        mTaskbarUIController.showEduOnAppLaunch();
+                    }
+                });
     }
 
     public boolean canStartHomeSafely() {
@@ -1436,6 +1454,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/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index d98e608..cb2c324 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -58,8 +58,6 @@
     /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/
     private boolean mCanIntercept;
 
-    private boolean mIsTrackpadReverseScroll;
-
     public StatusBarTouchController(Launcher l) {
         mLauncher = l;
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mLauncher);
@@ -95,8 +93,6 @@
             }
             mDownEvents.clear();
             mDownEvents.put(pid, new PointF(ev.getX(), ev.getY()));
-            mIsTrackpadReverseScroll = !mLauncher.isNaturalScrollingEnabled()
-                    && isTrackpadScroll(ev);
         } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
             // Check!! should only set it only when threshold is not entered.
             mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx)));
@@ -107,9 +103,6 @@
         if (action == ACTION_MOVE && mDownEvents.contains(pid)) {
             float dy = ev.getY(idx) - mDownEvents.get(pid).y;
             float dx = ev.getX(idx) - mDownEvents.get(pid).x;
-            if (mIsTrackpadReverseScroll) {
-                dy = -dy;
-            }
             // Currently input dispatcher will not do touch transfer if there are more than
             // one touch pointer. Hence, even if slope passed, only set the slippery flag
             // when there is single touch event. (context: InputDispatcher.cpp line 1445)
@@ -134,7 +127,6 @@
             mLauncher.getStatsLogManager().logger()
                     .log(LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN);
             setWindowSlippery(false);
-            mIsTrackpadReverseScroll = false;
             return true;
         }
         return true;
@@ -161,9 +153,9 @@
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
-        if (!mLauncher.isInState(LauncherState.NORMAL) ||
-                AbstractFloatingView.getTopOpenViewWithType(mLauncher,
-                        AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) {
+        if (isTrackpadScroll(ev) || !mLauncher.isInState(LauncherState.NORMAL)
+                || AbstractFloatingView.getTopOpenViewWithType(mLauncher,
+                AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) {
             return false;
         } else {
             // For NORMAL state, only listen if the event originated above the navbar height
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index 358f644..fbc0d14 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -34,7 +34,7 @@
     abstractFloatingViewHelper: AbstractFloatingViewHelper
 ) :
     SystemShortcut<RecentsViewContainer>(
-        R.drawable.ic_caption_desktop_button_foreground,
+        R.drawable.ic_desktop,
         R.string.recent_task_option_desktop,
         container,
         taskContainer.itemInfo,
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 0c2f29b..59e9f05 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -47,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;
@@ -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
     //
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index f414399..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;
@@ -334,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/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index c3d74bb..8478ac9 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -74,6 +74,7 @@
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -232,8 +233,11 @@
      *
      * @param cuj Should be an integer from {@link Cuj} or -1 if no CUJ needs to be logged for jank
      *            monitoring
+     * @param callback Called after the app pair launch finishes animating, or null if no method is
+     *                 to be called
      */
-    public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
+    public void launchAppPair(AppPairIcon appPairIcon, int cuj,
+            @Nullable Consumer<Boolean> callback) {
         WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp();
         WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp();
         ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
@@ -273,12 +277,19 @@
                     mSplitSelectStateController.setLaunchingIconView(appPairIcon);
 
                     mSplitSelectStateController.launchSplitTasks(
-                            AppPairsController.convertRankToSnapPosition(app1.rank));
+                            AppPairsController.convertRankToSnapPosition(app1.rank), callback);
                 }
         );
     }
 
     /**
+     * Launches an app pair but does not specify a callback
+     */
+    public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
+        launchAppPair(appPairIcon, cuj, null);
+    }
+
+    /**
      * Returns an AppInfo associated with the app for the given ComponentKey, or null if no such
      * package exists in the AllAppsStore.
      */
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 54f2dd3..770ec5a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -836,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,
@@ -847,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;
         }
 
@@ -954,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..aa628f8 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -19,7 +19,6 @@
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
-import android.graphics.drawable.LayerDrawable
 import android.graphics.drawable.ShapeDrawable
 import android.graphics.drawable.shapes.RoundRectShape
 import android.util.AttributeSet
@@ -27,9 +26,12 @@
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import androidx.core.content.res.ResourcesCompat
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
+import com.android.launcher3.testing.TestLogging
+import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TransformingTouchDelegate
@@ -84,9 +86,15 @@
             }
         iconView =
             getOrInflateIconView(R.id.icon).apply {
-                val iconBackground = resources.getDrawable(R.drawable.bg_circle, context.theme)
-                val icon = resources.getDrawable(R.drawable.ic_desktop, context.theme)
-                setIcon(this, LayerDrawable(arrayOf(iconBackground, icon)))
+                setIcon(
+                    this,
+                    ResourcesCompat.getDrawable(
+                        context.resources,
+                        R.drawable.ic_desktop_with_bg,
+                        context.theme
+                    )
+                )
+                setText(resources.getText(R.string.recent_task_option_desktop))
             }
         childCountAtInflation = childCount
     }
@@ -213,7 +221,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 +244,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" }
@@ -266,6 +287,7 @@
         private const val TAG = "DesktopTaskView"
         private const val DEBUG = false
         private const val VIEW_POOL_MAX_SIZE = 10
+
         // As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool.
         private const val VIEW_POOL_INITIAL_SIZE = 0
         private val ORIGIN = Point(0, 0)
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index d87e05f..176a538 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -901,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)
                 }
             }
         }
@@ -931,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) {
@@ -1152,6 +1160,11 @@
             isClickableAsLiveTile = true
             return runnableList
         }
+        TestLogging.recordEvent(
+            TestProtocol.SEQUENCE_MAIN,
+            "composeRecentsLaunchAnimator",
+            taskIds.contentToString()
+        )
         val runnableList = RunnableList()
         with(AnimatorSet()) {
             TaskViewUtils.composeRecentsLaunchAnimator(
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 0a3351d..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,12 +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
@@ -51,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
 
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 619ce1c..4da06e1 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
@@ -84,7 +84,7 @@
             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
@@ -98,7 +98,7 @@
         assertThat(bubbleBarView.scaleX).isEqualTo(1)
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
 
         // execute the hide bubble animation
         assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -111,7 +111,7 @@
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).stashBubbleBarImmediate()
     }
 
@@ -128,7 +128,7 @@
             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
@@ -142,7 +142,7 @@
         assertThat(bubbleBarView.scaleX).isEqualTo(1)
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
 
         verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion()
 
@@ -155,7 +155,7 @@
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
     }
 
     @Test
@@ -171,7 +171,7 @@
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // wait for the animation to start
@@ -179,7 +179,7 @@
         PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
 
         handleAnimator.assertIsRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
@@ -189,7 +189,7 @@
 
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
 
         // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
@@ -211,7 +211,7 @@
             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
@@ -230,7 +230,7 @@
             animator.onStashStateChangingWhileAnimating()
         }
 
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
 
         // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
@@ -252,7 +252,7 @@
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleInForStashed(bubble)
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
         }
 
         // wait for the animation to start
@@ -260,16 +260,133 @@
         PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
 
         handleAnimator.assertIsRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
         handleAnimator.cancel()
         handleAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         assertThat(animatorScheduler.delayedBlock).isNull()
     }
 
     @Test
+    fun animateBubbleInForStashed_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        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(animator.isAnimating).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(animator.isAnimating).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the animation finish
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isFalse()
+    }
+
+    @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(animator.isAnimating).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(animator.isAnimating).isFalse()
+    }
+
+    @Test
     fun animateToInitialState_inApp() {
         setUpBubbleBar()
         setUpBubbleStashController()
@@ -293,7 +410,7 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         barAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
 
@@ -304,7 +421,7 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(0)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(handle.alpha).isEqualTo(1)
@@ -336,17 +453,11 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         barAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
 
-        assertThat(animatorScheduler.delayedBlock).isNotNull()
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
-
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
-        assertThat(bubbleBarView.alpha).isEqualTo(1)
-        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-
+        assertThat(animatorScheduler.delayedBlock).isNull()
         verify(bubbleStashController).showBubbleBarImmediate()
     }
 
@@ -370,14 +481,14 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         barAnimator.assertIsNotRunning()
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
@@ -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(animator.isAnimating).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the animation finish
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @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(animator.isAnimating).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(animator.isAnimating).isFalse()
+    }
+
+    @Test
     fun animateBubbleBarForCollapsed() {
         setUpBubbleBar()
         setUpBubbleStashController()
@@ -397,12 +581,12 @@
             BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.animateBubbleBarForCollapsed(bubble)
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
         }
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         // verify we started animating
-        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animator.isAnimating).isTrue()
 
         // advance the animation handler by the duration of the initial lift
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -417,13 +601,149 @@
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
-        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animator.isAnimating).isFalse()
         // the bubble bar translation y should be back to its initial value
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
         verify(bubbleStashController).showBubbleBarImmediate()
     }
 
+    @Test
+    fun animateBubbleBarForCollapsed_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleBarForCollapsed(bubble, isExpanding = true)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        // verify we started animating
+        assertThat(animator.isAnimating).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there is no hide animation
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @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(animator.isAnimating).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true }
+
+        // verify there is a pending hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        assertThat(animator.isAnimating).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(animator.isAnimating).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(animator.isAnimating).isTrue()
+
+        // advance the animation handler by the duration of the initial lift
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        // the lift animation is complete; the spring back animation should start now
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there is a pending hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        assertThat(animator.isAnimating).isTrue()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+        assertThat(bubbleBarView.isExpanded).isTrue()
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -459,6 +779,14 @@
             .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/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/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/res/drawable/ic_bubble_button.xml b/res/drawable/ic_bubble_button.xml
new file mode 100644
index 0000000..1ed212e
--- /dev/null
+++ b/res/drawable/ic_bubble_button.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="20dp"
+        android:height="20dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M23,5v8h-2V5H3v14h10v2v0H3c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h18C22.1,3 23,3.9 23,5zM10,8v2.59L5.71,6.29L4.29,7.71L8.59,12H6v2h6V8H10zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/>
+</vector>
diff --git a/res/drawable/ic_desktop_with_bg.xml b/res/drawable/ic_desktop_with_bg.xml
new file mode 100644
index 0000000..f54285c
--- /dev/null
+++ b/res/drawable/ic_desktop_with_bg.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M24,2L24,2A22,22 0,0 1,46 24L46,24A22,22 0,0 1,24 46L24,46A22,22 0,0 1,2 24L2,24A22,22 0,0 1,24 2z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="M32,30H16V18H32V30ZM32,32C33.1,32 34,31.1 34,30V18C34,16.9 33.1,16 32,16H16C14.9,16 14,16.9 14,18V30C14,31.1 14.9,32 16,32H32ZM30,20H23V22H28V24H30V20ZM18,23H27V28H18V23Z"
+      android:fillColor="#000000"
+      android:fillType="evenOdd"/>
+</vector>
diff --git a/res/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/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 2eb5034..1eccbff 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -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/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/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 78ce3a2..6ff51ca 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -18,9 +18,6 @@
 
 import android.content.Context
 import android.util.Log
-import android.view.ViewGroup
-import androidx.annotation.VisibleForTesting
-import androidx.annotation.VisibleForTesting.Companion.PROTECTED
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
@@ -34,7 +31,6 @@
 import com.android.launcher3.util.Themes
 import com.android.launcher3.views.ActivityContext
 import com.android.launcher3.views.ActivityContext.ActivityContextDelegate
-import java.lang.IllegalStateException
 
 const val PREINFLATE_ICONS_ROW_COUNT = 4
 const val EXTRA_ICONS_COUNT = 2
@@ -44,11 +40,10 @@
  * [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"
@@ -59,7 +54,7 @@
     /**
      * 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)
@@ -103,52 +98,36 @@
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
-        preInflateAllAppsViewHolders(
-            adapter,
-            BaseAllAppsAdapter.VIEW_TYPE_ICON,
-            activeRv,
-            preInflateCount
-        ) {
-            getPreinflateCount(context)
-        }
-    }
-
-    @VisibleForTesting(otherwise = PROTECTED)
-    fun preInflateAllAppsViewHolders(
-        adapter: RecyclerView.Adapter<*>,
-        viewType: Int,
-        parent: ViewGroup,
-        preInflationCount: Int,
-        preInflationCountProvider: () -> Int
-    ) {
-        if (preInflationCount <= 0) {
-            return
-        }
         mCancellableTask?.cancel()
         var task: CancellableTask<List<ViewHolder>>? = null
         task =
             CancellableTask(
                 {
                     val list: ArrayList<ViewHolder> = ArrayList()
-                    for (i in 0 until preInflationCount) {
+                    for (i in 0 until preInflateCount) {
                         if (task?.canceled == true) {
                             break
                         }
-                        list.add(adapter.createViewHolder(parent, viewType))
+                        // If activeRv's layout manager has been reset to null on main thread, skip
+                        // the preinflation as we cannot generate correct LayoutParams
+                        if (activeRv.layoutManager == null) {
+                            break
+                        }
+                        list.add(
+                            adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+                        )
                     }
                     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)
     }
 
     /**
@@ -169,7 +148,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/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/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/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
deleted file mode 100644
index 3e6aae2..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) { 10 }
-
-        awaitTasksCompleted()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
-    }
-
-    @Test
-    fun preinflate_not_triggered() {
-        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 }
-
-        awaitTasksCompleted()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
-    }
-
-    @Test
-    fun preinflate_cancel_before_runOnMainThread() {
-        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
-        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
-
-        underTest.clear()
-
-        awaitTasksCompleted()
-        verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
-        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
-    }
-
-    @Test
-    fun preinflate_cancel_after_run() {
-        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
-        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
-        awaitTasksCompleted()
-
-        underTest.clear()
-
-        verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
-        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
-        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
-    }
-
-    private fun awaitTasksCompleted() {
-        Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
-        Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
-    }
-
-    companion object {
-        private const val VIEW_TYPE: Int = 4
-    }
-}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 1e2744c..749a75a 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -43,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;
 
@@ -222,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();
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();
-    }
-}