Merge "Add smartspace as a widget to first page (implementation 2)" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 9221f6b..874f862 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -34,3 +34,10 @@
     description: "Enables new workspace grid calculations method."
     bug: "241386436"
 }
+
+flag {
+    name: "enable_overview_icon_menu"
+    namespace: "launcher"
+    description: "Enable updated overview icon and menu within task."
+    bug: "257950105"
+}
diff --git a/quickstep/res/drawable/ic_chevron_down.xml b/quickstep/res/drawable/ic_chevron_down.xml
new file mode 100644
index 0000000..77a8295
--- /dev/null
+++ b/quickstep/res/drawable/ic_chevron_down.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <target android:name="scaleGroup">
+        <aapt:attr name="android:animation">
+            <objectAnimator
+                android:duration="150"
+                android:propertyName="scaleX"
+                android:valueFrom="1"
+                android:valueTo="-1" />
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="48dp"
+            android:height="48dp"
+            android:autoMirrored="true"
+            android:tint="?androidprv:attr/materialColorOnSurface"
+            android:viewportHeight="48"
+            android:viewportWidth="48">
+            <group
+                android:name="scaleGroup"
+                android:pivotX="24"
+                android:pivotY="24"
+                android:rotation="90">
+                <path
+                    android:fillColor="@android:color/white"
+                    android:pathData="M18.75,36 L16.6,33.85 26.5,23.95 16.6,14.05 18.75,11.9 30.8,23.95Z" />
+            </group>
+        </vector>
+    </aapt:attr>
+</animated-vector>
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
new file mode 100644
index 0000000..87519a0
--- /dev/null
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.quickstep.views.IconAppChipView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/icon"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:focusable="false"
+    android:importantForAccessibility="no"
+    android:autoMirrored="true"
+    android:elevation="@dimen/task_thumbnail_icon_menu_elevation" >
+
+    <ImageView
+        android:id="@+id/icon_view_background"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/icon_menu_background"
+        android:importantForAccessibility="no" />
+
+    <com.android.quickstep.views.IconView
+        android:id="@+id/icon_view"
+        android:layout_width="@dimen/task_thumbnail_icon_size"
+        android:layout_height="@dimen/task_thumbnail_icon_size"
+        android:focusable="false"
+        android:importantForAccessibility="no" />
+
+    <TextView
+        android:id="@+id/icon_text"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:maxLines="1"
+        android:textAlignment="viewStart"
+        android:textColor="?androidprv:attr/materialColorOnSurface"
+        android:textSize="16sp"
+        android:importantForAccessibility="no" />
+
+    <ImageView
+        android:id="@+id/icon_arrow"
+        android:layout_width="@dimen/task_thumbnail_icon_menu_arrow_size"
+        android:layout_height="match_parent"
+        android:background="@drawable/icon_menu_arrow_background"
+        android:src="@drawable/ic_chevron_down"
+        android:importantForAccessibility="no" />
+</com.android.quickstep.views.IconAppChipView>
\ No newline at end of file
diff --git a/quickstep/res/layout/icon_view.xml b/quickstep/res/layout/icon_view.xml
new file mode 100644
index 0000000..a33066f
--- /dev/null
+++ b/quickstep/res/layout/icon_view.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.quickstep.views.IconView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 29c9992..823a86e 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -44,10 +44,9 @@
         android:importantForAccessibility="no"
         android:src="@drawable/ic_select_windows" />
 
-    <com.android.quickstep.views.IconView
+    <ViewStub
         android:id="@+id/icon"
-        android:layout_width="@dimen/task_thumbnail_icon_size"
-        android:layout_height="@dimen/task_thumbnail_icon_size"
-        android:focusable="false"
-        android:importantForAccessibility="no"/>
+        android:inflatedId="@id/icon"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content" />
 </com.android.quickstep.views.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 06f4d06..fe12bd3 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -45,11 +45,10 @@
         android:layout_height="wrap_content"
         android:visibility="gone" />
 
-    <com.android.quickstep.views.IconView
+    <ViewStub
         android:id="@+id/icon"
-        android:layout_width="@dimen/task_thumbnail_icon_size"
-        android:layout_height="@dimen/task_thumbnail_icon_size"
-        android:focusable="false"
-        android:importantForAccessibility="no" />
+        android:inflatedId="@id/icon"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content" />
 
 </com.android.quickstep.views.DesktopTaskView>
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 75ff626..d20afd3 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -66,17 +66,15 @@
         android:importantForAccessibility="no"
         android:src="@drawable/ic_select_windows" />
 
-    <com.android.quickstep.views.IconView
+    <ViewStub
         android:id="@+id/icon"
-        android:layout_width="@dimen/task_thumbnail_icon_size"
-        android:layout_height="@dimen/task_thumbnail_icon_size"
-        android:focusable="false"
-        android:importantForAccessibility="no"/>
+        android:inflatedId="@id/icon"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content" />
 
-    <com.android.quickstep.views.IconView
+    <ViewStub
         android:id="@+id/bottomRight_icon"
-        android:layout_width="@dimen/task_thumbnail_icon_size"
-        android:layout_height="@dimen/task_thumbnail_icon_size"
-        android:focusable="false"
-        android:importantForAccessibility="no"/>
+        android:inflatedId="@id/bottomRight_icon"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content" />
 </com.android.quickstep.views.GroupedTaskView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index aeb453c..aaa699b 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -44,6 +44,33 @@
     <dimen name="overview_task_margin">16dp</dimen>
     <!--  The horizontal space between tasks  -->
     <dimen name="overview_page_spacing">16dp</dimen>
+    <!--  The width of the thumbnail icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_min_width">132dp</dimen>
+    <!--  The width of the icon menu text  -->
+    <dimen name="task_thumbnail_icon_menu_text_width">62dp</dimen>
+    <!--  The max width of the icon menu text  -->
+    <dimen name="task_thumbnail_icon_menu_text_max_width">138dp</dimen>
+    <!--  The max width of the thumbnail icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_max_width">216dp</dimen>
+    <!--  The height of the thumbnail icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_min_height">36dp</dimen>
+    <!--  The max height of the thumbnail icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_max_height">52dp</dimen>
+    <!--  The size of the icon menu arrow  -->
+    <dimen name="task_thumbnail_icon_menu_arrow_size">32dp</dimen>
+    <!--  The size of the icon menu arrow drawable  -->
+    <dimen name="task_thumbnail_icon_menu_arrow_drawable_size">16dp</dimen>
+    <!--  The margin around the task icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_margin">12dp</dimen>
+    <!--  The space around the task icon arrow within the icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_arrow_margin">6dp</dimen>
+    <!--  The max space around the task icon within the icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_touch_max_margin">8dp</dimen>
+    <!--  The icon size for the icon menu  -->
+    <dimen name="task_thumbnail_icon_menu_drawable_size">24dp</dimen>
+    <!--  The size of the icon menu's icon touch target  -->
+    <dimen name="task_thumbnail_icon_menu_drawable_touch_size">36dp</dimen>
+    <dimen name="task_thumbnail_icon_menu_elevation">14dp</dimen>
 
     <dimen name="task_icon_cache_default_icon_size">72dp</dimen>
     <item name="overview_modal_max_scale" format="float" type="dimen">1.1</item>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 841fc8f..ce644dc 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1850,7 +1850,7 @@
                     return null;
                 }
 
-                current = (View) view.getParent();
+                current = (View) current.getParent();
             }
 
             return (T) current;
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 3e1a6ae..a9d50b9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -40,6 +40,8 @@
 
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * A view that displays a recent task during a keyboard quick switch.
  */
@@ -96,17 +98,18 @@
         Resources resources = mContext.getResources();
 
         Preconditions.assertNotNull(mContent);
-        mBorderAnimator = new BorderAnimator(
+        mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
                 /* borderRadiusPx= */ resources.getDimensionPixelSize(
                         R.dimen.keyboard_quick_switch_task_view_radius),
-                /* borderColor= */ mBorderColor,
-                /* borderAnimationParams= */ new BorderAnimator.ScalingParams(
-                        /* borderWidthPx= */ resources.getDimensionPixelSize(
+                /* borderWidthPx= */ resources.getDimensionPixelSize(
                                 R.dimen.keyboard_quick_switch_border_width),
-                        /* boundsBuilder= */ bounds -> bounds.set(
-                                0, 0, getWidth(), getHeight()),
-                        /* targetView= */ this,
-                        /* contentView= */ mContent));
+                /* boundsBuilder= */ bounds -> {
+                    bounds.set(0, 0, getWidth(), getHeight());
+                    return Unit.INSTANCE;
+                },
+                /* targetView= */ this,
+                /* contentView= */ mContent,
+                /* borderColor= */ mBorderColor);
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index e6dfe0f..1c7d7e0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -337,7 +337,7 @@
     }
 
     public boolean isBubbleBarEnabled() {
-        return BubbleBarController.BUBBLE_BAR_ENABLED;
+        return BubbleBarController.isBubbleBarEnabled();
     }
 
     /** Whether the bubble bar has any bubbles. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index df6626a..4b16019 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -191,6 +191,7 @@
     private MultiValueAlpha mBackButtonAlpha;
     private MultiValueAlpha mHomeButtonAlpha;
     private FloatingRotationButton mFloatingRotationButton;
+    private ImageView mImeSwitcherButton;
 
     // Variables for moving nav buttons to a separate window above IME
     private boolean mAreNavButtonsInSeparateWindow = false;
@@ -237,10 +238,10 @@
                 InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar();
         if (!mIsImeRenderingNavButtons) {
             // IME switcher
-            View imeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH,
+            mImeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH,
                     isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer,
                     mControllers.navButtonController, R.id.ime_switcher);
-            mPropertyHolders.add(new StatePropertyHolder(imeSwitcherButton,
+            mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton,
                     flags -> ((flags & FLAG_SWITCHER_SHOWING) != 0)
                             && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0)));
         }
@@ -736,7 +737,9 @@
         if (TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) {
             NavButtonLayoutter navButtonLayoutter =
                     NavButtonLayoutFactory.Companion.getUiLayoutter(
-                            dp, mNavButtonsView, res, isInKidsMode, isInSetup, isThreeButtonNav,
+                            dp, mNavButtonsView, mImeSwitcherButton,
+                            mControllers.rotationButtonController.getRotationButton(),
+                            mA11yButton, res, isInKidsMode, isInSetup, isThreeButtonNav,
                             TaskbarManager.isPhoneMode(dp),
                             mWindowManagerProxy.getRotation(mContext));
             navButtonLayoutter.layoutButtons(dp, isContextualButtonShowing());
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 0aa02f2..9f8f82a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -218,7 +218,8 @@
 
         // If Bubble bar is present, TaskbarControllers depends on it so build it first.
         Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
-        if (BubbleBarController.BUBBLE_BAR_ENABLED && bubbleBarView != null) {
+        BubbleBarController.onTaskbarRecreated();
+        if (BubbleBarController.isBubbleBarEnabled() && bubbleBarView != null) {
             bubbleControllersOptional = Optional.of(new BubbleControllers(
                     new BubbleBarController(this, bubbleBarView),
                     new BubbleBarViewController(this, bubbleBarView),
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index a0ce976..712374d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -17,7 +17,7 @@
 
 import static android.view.View.VISIBLE;
 
-import static com.android.launcher3.taskbar.bubbles.BubbleBarController.BUBBLE_BAR_ENABLED;
+import static com.android.launcher3.taskbar.bubbles.BubbleBarController.isBubbleBarEnabled;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
@@ -83,7 +83,7 @@
      * Updates the scrim state based on the flags.
      */
     public void updateStateForSysuiFlags(int stateFlags, boolean skipAnim) {
-        if (BUBBLE_BAR_ENABLED && DisplayController.isTransientTaskbar(mActivity)) {
+        if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) {
             // These scrims aren't used if bubble bar & transient taskbar are active.
             return;
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index bd11efd..3fb7247 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -85,17 +85,31 @@
  * information to render each of the bubbles & dispatches changes to
  * {@link BubbleBarViewController} which will then update {@link BubbleBarView} as needed.
  *
- * For details around the behavior of the bubble bar, see {@link BubbleBarView}.
+ * <p>For details around the behavior of the bubble bar, see {@link BubbleBarView}.
  */
 public class BubbleBarController extends IBubblesListener.Stub {
 
     private static final String TAG = BubbleBarController.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    // Whether bubbles are showing in the bubble bar from launcher
-    public static final boolean BUBBLE_BAR_ENABLED =
+    /**
+     * Determines whether bubbles can be shown in the bubble bar. This value updates when the
+     * taskbar is recreated.
+     *
+     * @see #onTaskbarRecreated()
+     */
+    private static boolean sBubbleBarEnabled =
             SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
 
+    /** Whether showing bubbles in the launcher bubble bar is enabled. */
+    public static boolean isBubbleBarEnabled() {
+        return sBubbleBarEnabled;
+    }
+
+    /** Re-reads the value of the flag from SystemProperties when taskbar is recreated. */
+    public static void onTaskbarRecreated() {
+        sBubbleBarEnabled = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
+    }
     private static final int MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING
             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
@@ -167,7 +181,7 @@
 
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
 
-        if (BUBBLE_BAR_ENABLED) {
+        if (sBubbleBarEnabled) {
             mSystemUiProxy.setBubblesListener(this);
         }
         mMainExecutor = MAIN_EXECUTOR;
@@ -191,9 +205,9 @@
 
         bubbleControllers.runAfterInit(() -> {
             mBubbleBarViewController.setHiddenForBubbles(
-                    !BUBBLE_BAR_ENABLED || mBubbles.isEmpty());
+                    !sBubbleBarEnabled || mBubbles.isEmpty());
             mBubbleStashedHandleViewController.setHiddenForBubbles(
-                    !BUBBLE_BAR_ENABLED || mBubbles.isEmpty());
+                    !sBubbleBarEnabled || mBubbles.isEmpty());
             mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
                     key -> setSelectedBubble(mBubbles.get(key)));
         });
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
index b682081..9758d44 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
@@ -18,12 +18,15 @@
 
 import android.content.res.Resources
 import android.graphics.drawable.RotateDrawable
+import android.view.Gravity
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter
+import com.android.systemui.shared.rotation.RotationButton
 
 /**
  * Meant to be a simple container for data subclasses will need
@@ -37,10 +40,13 @@
  * @property startContextualContainer ViewGroup that holds the start contextual button (ex, A11y).
  */
 abstract class AbstractNavButtonLayoutter(
-    val resources: Resources,
-    val navButtonContainer: LinearLayout,
-    protected val endContextualContainer: ViewGroup,
-    protected val startContextualContainer: ViewGroup
+        val resources: Resources,
+        val navButtonContainer: LinearLayout,
+        protected val endContextualContainer: ViewGroup,
+        protected val startContextualContainer: ViewGroup,
+        protected val imeSwitcher: ImageView?,
+        protected val rotationButton: RotationButton?,
+        protected val a11yButton: ImageView
 ) : NavButtonLayoutter {
     protected val homeButton: ImageView? = navButtonContainer.findViewById(R.id.home)
     protected val recentsButton: ImageView? = navButtonContainer.findViewById(R.id.recent_apps)
@@ -56,4 +62,11 @@
             backButton.setImageDrawable(rotateDrawable)
         }
     }
+
+    fun getParamsToCenterView(): FrameLayout.LayoutParams {
+        val params = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        params.gravity = Gravity.CENTER
+        return params;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
index 4a53c0c..f254ee8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
@@ -26,18 +26,25 @@
 import android.widget.LinearLayout
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.*
+import com.android.systemui.shared.rotation.RotationButton
 
 class KidsNavLayoutter(
-    resources: Resources,
-    navBarContainer: LinearLayout,
-    endContextualContainer: ViewGroup,
-    startContextualContainer: ViewGroup
+        resources: Resources,
+        navBarContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup,
+        imeSwitcher: ImageView?,
+        rotationButton: RotationButton?,
+        a11yButton: ImageView
 ) :
     AbstractNavButtonLayoutter(
-        resources,
-        navBarContainer,
-        endContextualContainer,
-        startContextualContainer
+            resources,
+            navBarContainer,
+            endContextualContainer,
+            startContextualContainer,
+            imeSwitcher,
+            rotationButton,
+            a11yButton
     ) {
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
@@ -89,5 +96,28 @@
         navButtonContainer.requestLayout()
 
         homeButton.onLongClickListener = null
+
+        endContextualContainer.removeAllViews()
+        startContextualContainer.removeAllViews()
+
+        val endContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        endContextualContainerParams.gravity = Gravity.END or Gravity.CENTER_VERTICAL
+        endContextualContainer.layoutParams = endContextualContainerParams
+
+        val startContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        startContextualContainerParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
+        startContextualContainer.layoutParams = startContextualContainerParams
+
+        if (imeSwitcher != null) {
+            startContextualContainer.addView(imeSwitcher)
+            imeSwitcher.layoutParams = getParamsToCenterView()
+        }
+        endContextualContainer.addView(a11yButton)
+        if (rotationButton != null) {
+            endContextualContainer.addView(rotationButton.currentView)
+            rotationButton.currentView.layoutParams = getParamsToCenterView()
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 931f692..7db1a37 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -21,11 +21,13 @@
 import android.view.Surface.Rotation
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.*
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.Companion
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter
+import com.android.systemui.shared.rotation.RotationButton
 
 /**
  * Select the correct layout for nav buttons
@@ -52,14 +54,17 @@
          * @param isThreeButtonNav are no-ops when taskbar is present/showing
          */
         fun getUiLayoutter(
-            deviceProfile: DeviceProfile,
-            navButtonsView: FrameLayout,
-            resources: Resources,
-            isKidsMode: Boolean,
-            isInSetup: Boolean,
-            isThreeButtonNav: Boolean,
-            phoneMode: Boolean,
-            @Rotation surfaceRotation: Int
+                deviceProfile: DeviceProfile,
+                navButtonsView: FrameLayout,
+                imeSwitcher: ImageView?,
+                rotationButton: RotationButton?,
+                a11yButton: ImageView,
+                resources: Resources,
+                isKidsMode: Boolean,
+                isInSetup: Boolean,
+                isThreeButtonNav: Boolean,
+                phoneMode: Boolean,
+                @Rotation surfaceRotation: Int
         ): NavButtonLayoutter {
             val navButtonContainer =
                 navButtonsView.requireViewById<LinearLayout>(ID_END_NAV_BUTTONS)
@@ -73,24 +78,33 @@
                 isPhoneNavMode -> {
                     if (!deviceProfile.isLandscape) {
                         PhonePortraitNavLayoutter(
-                            resources,
-                            navButtonContainer,
-                            endContextualContainer,
-                            startContextualContainer
+                                resources,
+                                navButtonContainer,
+                                endContextualContainer,
+                                startContextualContainer,
+                                imeSwitcher,
+                                rotationButton,
+                                a11yButton
                         )
                     } else if (surfaceRotation == ROTATION_90) {
                         PhoneLandscapeNavLayoutter(
-                            resources,
-                            navButtonContainer,
-                            endContextualContainer,
-                            startContextualContainer
+                                resources,
+                                navButtonContainer,
+                                endContextualContainer,
+                                startContextualContainer,
+                                imeSwitcher,
+                                rotationButton,
+                                a11yButton
                         )
                     } else {
                         PhoneSeascapeNavLayoutter(
                                 resources,
                                 navButtonContainer,
                                 endContextualContainer,
-                                startContextualContainer
+                                startContextualContainer,
+                                imeSwitcher,
+                                rotationButton,
+                                a11yButton
                         )
                     }
                 }
@@ -99,33 +113,45 @@
                             resources,
                             navButtonContainer,
                             endContextualContainer,
-                            startContextualContainer
+                            startContextualContainer,
+                            imeSwitcher,
+                            rotationButton,
+                            a11yButton
                     )
                 }
                 deviceProfile.isTaskbarPresent -> {
                     return when {
                         isInSetup -> {
                             SetupNavLayoutter(
-                                resources,
-                                navButtonContainer,
-                                endContextualContainer,
-                                startContextualContainer
+                                    resources,
+                                    navButtonContainer,
+                                    endContextualContainer,
+                                    startContextualContainer,
+                                    imeSwitcher,
+                                    rotationButton,
+                                    a11yButton
                             )
                         }
                         isKidsMode -> {
                             KidsNavLayoutter(
-                                resources,
-                                navButtonContainer,
-                                endContextualContainer,
-                                startContextualContainer
+                                    resources,
+                                    navButtonContainer,
+                                    endContextualContainer,
+                                    startContextualContainer,
+                                    imeSwitcher,
+                                    rotationButton,
+                                    a11yButton
                             )
                         }
                         else ->
                             TaskbarNavLayoutter(
-                                resources,
-                                navButtonContainer,
-                                endContextualContainer,
-                                startContextualContainer
+                                    resources,
+                                    navButtonContainer,
+                                    endContextualContainer,
+                                    startContextualContainer,
+                                    imeSwitcher,
+                                    rotationButton,
+                                    a11yButton
                             )
                     }
                 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
index 8525c6c..c1dae40 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
@@ -18,24 +18,33 @@
 
 import android.content.res.Resources
 import android.view.ViewGroup
+import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.DeviceProfile
+import com.android.systemui.shared.rotation.RotationButton
 
 /** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */
 class PhoneGestureLayoutter(
         resources: Resources,
         navBarContainer: LinearLayout,
         endContextualContainer: ViewGroup,
-        startContextualContainer: ViewGroup
+        startContextualContainer: ViewGroup,
+        imeSwitcher: ImageView?,
+        rotationButton: RotationButton?,
+        a11yButton: ImageView
 ) :
         AbstractNavButtonLayoutter(
                 resources,
                 navBarContainer,
                 endContextualContainer,
-                startContextualContainer
+                startContextualContainer,
+                imeSwitcher,
+                rotationButton,
+                a11yButton
         ) {
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
-        // no-op
+        endContextualContainer.removeAllViews()
+        startContextualContainer.removeAllViews()
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index 13ffe6e..21bbca5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -20,24 +20,32 @@
 import android.view.Gravity
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import android.widget.ImageView
 import android.widget.LinearLayout
 import androidx.core.view.children
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.util.DimensionUtils
+import com.android.systemui.shared.rotation.RotationButton
 
 open class PhoneLandscapeNavLayoutter(
-    resources: Resources,
-    navBarContainer: LinearLayout,
-    endContextualContainer: ViewGroup,
-    startContextualContainer: ViewGroup
+        resources: Resources,
+        navBarContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup,
+        imeSwitcher: ImageView?,
+        rotationButton: RotationButton?,
+        a11yButton: ImageView,
 ) :
     AbstractNavButtonLayoutter(
-        resources,
-        navBarContainer,
-        endContextualContainer,
-        startContextualContainer
+            resources,
+            navBarContainer,
+            endContextualContainer,
+            startContextualContainer,
+            imeSwitcher,
+            rotationButton,
+            a11yButton
     ) {
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
@@ -81,6 +89,24 @@
                 }
             }
         }
+
+        endContextualContainer.removeAllViews()
+        startContextualContainer.removeAllViews()
+
+        val startContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        startContextualContainerParams.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
+        startContextualContainer.layoutParams = startContextualContainerParams
+
+        if (imeSwitcher != null) {
+            startContextualContainer.addView(imeSwitcher)
+            imeSwitcher.layoutParams = getParamsToCenterView()
+        }
+        startContextualContainer.addView(a11yButton)
+        if (rotationButton != null) {
+            startContextualContainer.addView(rotationButton.currentView)
+            rotationButton.currentView.layoutParams = getParamsToCenterView()
+        }
     }
 
     open fun addThreeButtons() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
index c763115..ad03e5b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
@@ -20,23 +20,31 @@
 import android.view.Gravity
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.util.DimensionUtils
+import com.android.systemui.shared.rotation.RotationButton
 
 class PhonePortraitNavLayoutter(
-    resources: Resources,
-    navBarContainer: LinearLayout,
-    endContextualContainer: ViewGroup,
-    startContextualContainer: ViewGroup
+        resources: Resources,
+        navBarContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup,
+        imeSwitcher: ImageView?,
+        rotationButton: RotationButton?,
+        a11yButton: ImageView,
 ) :
     AbstractNavButtonLayoutter(
-        resources,
-        navBarContainer,
-        endContextualContainer,
-        startContextualContainer
+            resources,
+            navBarContainer,
+            endContextualContainer,
+            startContextualContainer,
+            imeSwitcher,
+            rotationButton,
+            a11yButton
     ) {
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
@@ -90,5 +98,23 @@
                 }
             }
         }
+
+        endContextualContainer.removeAllViews()
+        startContextualContainer.removeAllViews()
+
+        val endContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        endContextualContainerParams.gravity = Gravity.END or Gravity.CENTER_VERTICAL
+        endContextualContainer.layoutParams = endContextualContainerParams
+
+        if (imeSwitcher != null) {
+            endContextualContainer.addView(imeSwitcher)
+            imeSwitcher.layoutParams = getParamsToCenterView()
+        }
+        endContextualContainer.addView(a11yButton)
+        if (rotationButton != null) {
+            endContextualContainer.addView(rotationButton.currentView)
+            rotationButton.currentView.layoutParams = getParamsToCenterView()
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
index 2d62c3f..cde39f3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
@@ -17,20 +17,30 @@
 package com.android.launcher3.taskbar.navbutton
 
 import android.content.res.Resources
+import android.view.Gravity
 import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
 import android.widget.LinearLayout
+import com.android.systemui.shared.rotation.RotationButton
 
 class PhoneSeascapeNavLayoutter(
         resources: Resources,
         navBarContainer: LinearLayout,
         endContextualContainer: ViewGroup,
-        startContextualContainer: ViewGroup
+        startContextualContainer: ViewGroup,
+        imeSwitcher: ImageView?,
+        rotationButton: RotationButton?,
+        a11yButton: ImageView
 ) :
         PhoneLandscapeNavLayoutter(
                 resources,
                 navBarContainer,
                 endContextualContainer,
-                startContextualContainer
+                startContextualContainer,
+                imeSwitcher,
+                rotationButton,
+                a11yButton
         ) {
 
     override fun addThreeButtons() {
@@ -38,5 +48,23 @@
         navButtonContainer.addView(backButton)
         navButtonContainer.addView(homeButton)
         navButtonContainer.addView(recentsButton)
+
+        endContextualContainer.removeAllViews()
+        startContextualContainer.removeAllViews()
+
+        val endContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        endContextualContainerParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
+        endContextualContainer.layoutParams = endContextualContainerParams
+
+        if (imeSwitcher != null) {
+            endContextualContainer.addView(imeSwitcher)
+            imeSwitcher.layoutParams = getParamsToCenterView()
+        }
+        endContextualContainer.addView(a11yButton)
+        if (rotationButton != null) {
+            endContextualContainer.addView(rotationButton.currentView)
+            rotationButton.currentView.layoutParams = getParamsToCenterView()
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
index a24002c..db245b8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
@@ -20,20 +20,28 @@
 import android.view.Gravity
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.DeviceProfile
+import com.android.systemui.shared.rotation.RotationButton
 
 class SetupNavLayoutter(
-    resources: Resources,
-    navButtonContainer: LinearLayout,
-    endContextualContainer: ViewGroup,
-    startContextualContainer: ViewGroup
+        resources: Resources,
+        navButtonContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup,
+        imeSwitcher: ImageView?,
+        rotationButton: RotationButton?,
+        a11yButton: ImageView
 ) :
     AbstractNavButtonLayoutter(
-        resources,
-        navButtonContainer,
-        endContextualContainer,
-        startContextualContainer
+            resources,
+            navButtonContainer,
+            endContextualContainer,
+            startContextualContainer,
+            imeSwitcher,
+            rotationButton,
+            a11yButton
     ) {
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
@@ -46,5 +54,28 @@
             gravity = Gravity.START
         }
         navButtonContainer.requestLayout()
+
+        endContextualContainer.removeAllViews()
+        startContextualContainer.removeAllViews()
+
+        val endContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        endContextualContainerParams.gravity = Gravity.END or Gravity.CENTER_VERTICAL
+        endContextualContainer.layoutParams = endContextualContainerParams
+
+        val startContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        startContextualContainerParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
+        startContextualContainer.layoutParams = startContextualContainerParams
+
+        if (imeSwitcher != null) {
+            startContextualContainer.addView(imeSwitcher)
+            imeSwitcher.layoutParams = getParamsToCenterView()
+        }
+        endContextualContainer.addView(a11yButton)
+        if (rotationButton != null) {
+            endContextualContainer.addView(rotationButton.currentView)
+            rotationButton.currentView.layoutParams = getParamsToCenterView()
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
index 8332b7d..56e55bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
@@ -20,22 +20,30 @@
 import android.view.Gravity
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
+import com.android.systemui.shared.rotation.RotationButton
 
 /** Layoutter for showing 3 button navigation on large screen */
 class TaskbarNavLayoutter(
-    resources: Resources,
-    navBarContainer: LinearLayout,
-    endContextualContainer: ViewGroup,
-    startContextualContainer: ViewGroup
+        resources: Resources,
+        navBarContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup,
+        imeSwitcher: ImageView?,
+        rotationButton: RotationButton?,
+        a11yButton: ImageView
 ) :
     AbstractNavButtonLayoutter(
-        resources,
-        navBarContainer,
-        endContextualContainer,
-        startContextualContainer
+            resources,
+            navBarContainer,
+            endContextualContainer,
+            startContextualContainer,
+            imeSwitcher,
+            rotationButton,
+            a11yButton
     ) {
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
@@ -77,5 +85,28 @@
                 }
             }
         }
+
+        endContextualContainer.removeAllViews()
+        startContextualContainer.removeAllViews()
+
+        val endContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        endContextualContainerParams.gravity = Gravity.END or Gravity.CENTER_VERTICAL
+        endContextualContainer.layoutParams = endContextualContainerParams
+
+        val startContextualContainerParams = FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        startContextualContainerParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
+        startContextualContainer.layoutParams = startContextualContainerParams
+
+        if (imeSwitcher != null) {
+            startContextualContainer.addView(imeSwitcher)
+            imeSwitcher.layoutParams = getParamsToCenterView()
+        }
+        endContextualContainer.addView(a11yButton)
+        if (rotationButton != null) {
+            endContextualContainer.addView(rotationButton.currentView)
+            rotationButton.currentView.layoutParams = getParamsToCenterView()
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 9be9294..221ce48 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -161,6 +161,10 @@
                 response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
                         SystemUiProxy.INSTANCE.get(mContext).isDragAndDropReady());
                 return response;
+
+            case TestProtocol.REQUEST_REFRESH_OVERVIEW_TARGET:
+                runOnTISBinder(TouchInteractionService.TISBinder::refreshOverviewTarget);
+                return response;
         }
 
         return super.call(method, arg, extras);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 02d0f39..02fcc68 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -409,18 +409,16 @@
          * Sets a proxy to bypass swipe up behavior
          */
         public void setSwipeUpProxy(Function<GestureState, AnimatedFloat> proxy) {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return;
-            tis.mSwipeUpProxyProvider = proxy != null ? proxy : (i -> null);
+            executeForTouchInteractionService(
+                    tis -> tis.mSwipeUpProxyProvider = proxy != null ? proxy : (i -> null));
         }
 
         /**
          * Sets the task id where gestures should be blocked
          */
         public void setGestureBlockedTaskId(int taskId) {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return;
-            tis.mDeviceState.setGestureBlockingTaskId(taskId);
+            executeForTouchInteractionService(
+                    tis -> tis.mDeviceState.setGestureBlockingTaskId(taskId));
         }
 
         /** Sets a listener to be run on Overview Target updates. */
@@ -434,6 +432,12 @@
                 mOnOverviewTargetChangeListener = null;
             }
         }
+
+        /** Refreshes the current overview target. */
+        public void refreshOverviewTarget() {
+            executeForTouchInteractionService(tis -> tis.onOverviewTargetChange(
+                    tis.mOverviewComponentObserver.isHomeAndOverviewSame()));
+        }
     }
 
     private static boolean sConnected = false;
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
deleted file mode 100644
index 7563187..0000000
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.java
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.ColorInt;
-import android.annotation.Nullable;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Px;
-
-import com.android.app.animation.Interpolators;
-import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.AnimatorListeners;
-
-/**
- * Utility class for drawing a rounded-rect border around a view.
- * <p>
- * To use this class:
- * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
- *      provided border bounds. See {@link SimpleParams} and {@link ScalingParams} to determine
- *      which would be best for your target view.
- * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call
- *      {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}.
- * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call
- *      {@link BorderAnimator#setBorderVisible(boolean)} where appropriate.
- */
-public final class BorderAnimator {
-
-    public static final int DEFAULT_BORDER_COLOR = Color.WHITE;
-
-    private static final long DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300;
-    private static final long DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133;
-    private static final Interpolator DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
-
-    @NonNull private final AnimatedFloat mBorderAnimationProgress = new AnimatedFloat(
-            this::updateOutline);
-    @Px private final int mBorderRadiusPx;
-    @NonNull private final BorderAnimationParams mBorderAnimationParams;
-    private final long mAppearanceDurationMs;
-    private final long mDisappearanceDurationMs;
-    @NonNull private final Interpolator mInterpolator;
-    @NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
-    @Nullable private Animator mRunningBorderAnimation;
-
-    public BorderAnimator(
-            @Px int borderRadiusPx,
-            @ColorInt int borderColor,
-            @NonNull BorderAnimationParams borderAnimationParams) {
-        this(borderRadiusPx,
-                borderColor,
-                borderAnimationParams,
-                DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
-                DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
-                DEFAULT_INTERPOLATOR);
-    }
-
-    /**
-     * @param borderRadiusPx the radius of the border's corners, in pixels
-     * @param borderColor the border's color
-     * @param borderAnimationParams params for handling different target view layout situation.
-     * @param appearanceDurationMs appearance animation duration, in milliseconds
-     * @param disappearanceDurationMs disappearance animation duration, in milliseconds
-     * @param interpolator animation interpolator
-     */
-    public BorderAnimator(
-            @Px int borderRadiusPx,
-            @ColorInt int borderColor,
-            @NonNull BorderAnimationParams borderAnimationParams,
-            long appearanceDurationMs,
-            long disappearanceDurationMs,
-            @NonNull Interpolator interpolator) {
-        mBorderRadiusPx = borderRadiusPx;
-        mBorderAnimationParams = borderAnimationParams;
-        mAppearanceDurationMs = appearanceDurationMs;
-        mDisappearanceDurationMs = disappearanceDurationMs;
-        mInterpolator = interpolator;
-
-        mBorderPaint.setColor(borderColor);
-        mBorderPaint.setStyle(Paint.Style.STROKE);
-        mBorderPaint.setAlpha(0);
-    }
-
-    private void updateOutline() {
-        float interpolatedProgress = mInterpolator.getInterpolation(
-                mBorderAnimationProgress.value);
-
-        mBorderAnimationParams.setProgress(interpolatedProgress);
-        mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress));
-        mBorderPaint.setStrokeWidth(mBorderAnimationParams.getBorderWidth());
-        mBorderAnimationParams.mTargetView.invalidate();
-    }
-
-    /**
-     * Draws the border on the given canvas.
-     * <p>
-     * Call this method in the target view's {@link android.view.View#draw(Canvas)} method after
-     * calling super.
-     */
-    public void drawBorder(Canvas canvas) {
-        float alignmentAdjustment = mBorderAnimationParams.getAlignmentAdjustment();
-        canvas.drawRoundRect(
-                /* left= */ mBorderAnimationParams.mBorderBounds.left + alignmentAdjustment,
-                /* top= */ mBorderAnimationParams.mBorderBounds.top + alignmentAdjustment,
-                /* right= */ mBorderAnimationParams.mBorderBounds.right - alignmentAdjustment,
-                /* bottom= */ mBorderAnimationParams.mBorderBounds.bottom - alignmentAdjustment,
-                /* rx= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
-                /* ry= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
-                /* paint= */ mBorderPaint);
-    }
-
-    /**
-     * Builds the border appearance/disappearance animation.
-     */
-    @NonNull
-    public Animator buildAnimator(boolean isAppearing) {
-        mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f);
-        mRunningBorderAnimation.setDuration(
-                isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs);
-
-        mRunningBorderAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                mBorderAnimationParams.onShowBorder();
-            }
-        });
-        mRunningBorderAnimation.addListener(
-                AnimatorListeners.forEndCallback(() -> {
-                    mRunningBorderAnimation = null;
-                    if (isAppearing) {
-                        return;
-                    }
-                    mBorderAnimationParams.onHideBorder();
-                }));
-
-        return mRunningBorderAnimation;
-    }
-
-    /**
-     * Immediately shows/hides the border without an animation.
-     * <p>
-     * To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)}
-     */
-    public void setBorderVisible(boolean visible) {
-        if (mRunningBorderAnimation != null) {
-            mRunningBorderAnimation.end();
-        }
-        if (visible) {
-            mBorderAnimationParams.onShowBorder();
-        }
-        mBorderAnimationProgress.updateValue(visible ? 1f : 0f);
-        if (!visible) {
-            mBorderAnimationParams.onHideBorder();
-        }
-    }
-
-    /**
-     * Callback to update the border bounds when building this animation.
-     */
-    public interface BorderBoundsBuilder {
-
-        /**
-         * Sets the given rect to the most up-to-date bounds.
-         */
-        void updateBorderBounds(Rect rect);
-    }
-
-    /**
-     * Params for handling different target view layout situation.
-     */
-    private abstract static class BorderAnimationParams {
-
-        @NonNull private final Rect mBorderBounds = new Rect();
-        @NonNull private final BorderBoundsBuilder mBoundsBuilder;
-
-        @NonNull final View mTargetView;
-        @Px final int mBorderWidthPx;
-
-        private float mAnimationProgress = 0f;
-        @Nullable private View.OnLayoutChangeListener mLayoutChangeListener;
-
-        /**
-         * @param borderWidthPx the width of the border, in pixels
-         * @param boundsBuilder callback to update the border bounds
-         * @param targetView the view that will be drawing the border
-         */
-        private BorderAnimationParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView) {
-            mBorderWidthPx = borderWidthPx;
-            mBoundsBuilder = boundsBuilder;
-            mTargetView = targetView;
-        }
-
-        private void setProgress(float progress) {
-            mAnimationProgress = progress;
-        }
-
-        private float getBorderWidth() {
-            return mBorderWidthPx * mAnimationProgress;
-        }
-
-        float getAlignmentAdjustment() {
-            // Outset the border by half the width to create an outwards-growth animation
-            return (-getBorderWidth() / 2f) + getAlignmentAdjustmentInset();
-        }
-
-
-        void onShowBorder() {
-            if (mLayoutChangeListener == null) {
-                mLayoutChangeListener =
-                        (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                            onShowBorder();
-                            mTargetView.invalidate();
-                        };
-                mTargetView.addOnLayoutChangeListener(mLayoutChangeListener);
-            }
-            mBoundsBuilder.updateBorderBounds(mBorderBounds);
-        }
-
-        void onHideBorder() {
-            if (mLayoutChangeListener != null) {
-                mTargetView.removeOnLayoutChangeListener(mLayoutChangeListener);
-                mLayoutChangeListener = null;
-            }
-        }
-
-        abstract int getAlignmentAdjustmentInset();
-
-        abstract float getRadiusAdjustment();
-    }
-
-    /**
-     * Use an instance of this {@link BorderAnimationParams} if the border can be drawn outside the
-     * target view's bounds without any additional logic.
-     */
-    public static final class SimpleParams extends BorderAnimationParams {
-
-        public SimpleParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView) {
-            super(borderWidthPx, boundsBuilder, targetView);
-        }
-
-        @Override
-        int getAlignmentAdjustmentInset() {
-            return 0;
-        }
-
-        @Override
-        float getRadiusAdjustment() {
-            return -getAlignmentAdjustment();
-        }
-    }
-
-    /**
-     * Use an instance of this {@link BorderAnimationParams} if the border would other be clipped by
-     * the target view's bound.
-     * <p>
-     * Note: using these params will set the scales and pivots of the
-     * container and content views, however will only reset the scales back to 1.
-     */
-    public static final class ScalingParams extends BorderAnimationParams {
-
-        @NonNull private final View mContentView;
-
-        /**
-         * @param targetView the view that will be drawing the border. this view will be scaled up
-         *                   to make room for the border
-         * @param contentView the view around which the border will be drawn. this view will be
-         *                    scaled down reciprocally to keep its original size and location.
-         */
-        public ScalingParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView,
-                @NonNull View contentView) {
-            super(borderWidthPx, boundsBuilder, targetView);
-            mContentView = contentView;
-        }
-
-        @Override
-        void onShowBorder() {
-            super.onShowBorder();
-            float width = mTargetView.getWidth();
-            float height = mTargetView.getHeight();
-            // Scale up just enough to make room for the border. Fail fast and fix the scaling
-            // onLayout.
-            float scaleX = width == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / width);
-            float scaleY = height == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / height);
-
-            mTargetView.setPivotX(width / 2);
-            mTargetView.setPivotY(height / 2);
-            mTargetView.setScaleX(scaleX);
-            mTargetView.setScaleY(scaleY);
-
-            mContentView.setPivotX(mContentView.getWidth() / 2f);
-            mContentView.setPivotY(mContentView.getHeight() / 2f);
-            mContentView.setScaleX(1f / scaleX);
-            mContentView.setScaleY(1f / scaleY);
-        }
-
-        @Override
-        void onHideBorder() {
-            super.onHideBorder();
-            mTargetView.setPivotX(mTargetView.getWidth());
-            mTargetView.setPivotY(mTargetView.getHeight());
-            mTargetView.setScaleX(1f);
-            mTargetView.setScaleY(1f);
-
-            mContentView.setPivotX(mContentView.getWidth() / 2f);
-            mContentView.setPivotY(mContentView.getHeight() / 2f);
-            mContentView.setScaleX(1f);
-            mContentView.setScaleY(1f);
-        }
-
-        @Override
-        int getAlignmentAdjustmentInset() {
-            // Inset the border since we are scaling the container up
-            return mBorderWidthPx;
-        }
-
-        @Override
-        float getRadiusAdjustment() {
-            // Increase the radius since we are scaling the container up
-            return getAlignmentAdjustment();
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
new file mode 100644
index 0000000..44eb070
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util
+
+import android.animation.Animator
+import android.annotation.ColorInt
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.animation.Interpolator
+import androidx.annotation.Px
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import com.android.app.animation.Interpolators
+import com.android.launcher3.anim.AnimatedFloat
+import kotlin.math.roundToInt
+
+/**
+ * Utility class for drawing a rounded-rect border around a view.
+ *
+ * To use this class:
+ * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
+ *    provided border bounds.
+ * 2. Override the target view's [View.draw] method and call [drawBorder] after
+ *    `super.draw(canvas)`.
+ * 3. Call [buildAnimator] and start the animation or call [setBorderVisibility] where appropriate.
+ */
+class BorderAnimator
+private constructor(
+    @field:Px @param:Px private val borderRadiusPx: Int,
+    @ColorInt borderColor: Int,
+    private val borderAnimationParams: BorderAnimationParams,
+    private val appearanceDurationMs: Long,
+    private val disappearanceDurationMs: Long,
+    private val interpolator: Interpolator,
+) {
+    private val borderAnimationProgress = AnimatedFloat { updateOutline() }
+    private val borderPaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).apply {
+            color = borderColor
+            style = Paint.Style.STROKE
+            alpha = 0
+        }
+    private var runningBorderAnimation: Animator? = null
+
+    companion object {
+        const val DEFAULT_BORDER_COLOR = Color.WHITE
+        private const val DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300L
+        private const val DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133L
+        private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE
+
+        /**
+         * Creates a BorderAnimator that simply draws the border outside the bound of the target
+         * view.
+         *
+         * Use this method if the border can be drawn outside the target view's bounds without any
+         * additional logic.
+         *
+         * @param borderRadiusPx the radius of the border's corners, in pixels
+         * @param borderWidthPx the width of the border, in pixels
+         * @param boundsBuilder callback to update the border bounds
+         * @param targetView the view that will be drawing the border
+         * @param borderColor the border's color
+         * @param appearanceDurationMs appearance animation duration, in milliseconds
+         * @param disappearanceDurationMs disappearance animation duration, in milliseconds
+         * @param interpolator animation interpolator
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun createSimpleBorderAnimator(
+            @Px borderRadiusPx: Int,
+            @Px borderWidthPx: Int,
+            boundsBuilder: (rect: Rect?) -> Unit,
+            targetView: View,
+            @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
+            appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
+            disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
+            interpolator: Interpolator = DEFAULT_INTERPOLATOR,
+        ): BorderAnimator {
+            return BorderAnimator(
+                borderRadiusPx,
+                borderColor,
+                SimpleParams(borderWidthPx, boundsBuilder, targetView),
+                appearanceDurationMs,
+                disappearanceDurationMs,
+                interpolator,
+            )
+        }
+
+        /**
+         * Creates a BorderAnimator that scales the target and content views to draw the border
+         * within the target's bounds without obscuring the content.
+         *
+         * Use this method if the border would otherwise be clipped by the target view's bound.
+         *
+         * Note: using this method will set the scales and pivots of the container and content
+         * views, however will only reset the scales back to 1.
+         *
+         * @param borderRadiusPx the radius of the border's corners, in pixels
+         * @param borderWidthPx the width of the border, in pixels
+         * @param boundsBuilder callback to update the border bounds
+         * @param targetView the view that will be drawing the border
+         * @param contentView the view around which the border will be drawn. this view will be
+         *   scaled down reciprocally to keep its original size and location.
+         * @param borderColor the border's color
+         * @param appearanceDurationMs appearance animation duration, in milliseconds
+         * @param disappearanceDurationMs disappearance animation duration, in milliseconds
+         * @param interpolator animation interpolator
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun createScalingBorderAnimator(
+            @Px borderRadiusPx: Int,
+            @Px borderWidthPx: Int,
+            boundsBuilder: (rect: Rect?) -> Unit,
+            targetView: View,
+            contentView: View,
+            @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
+            appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
+            disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
+            interpolator: Interpolator = DEFAULT_INTERPOLATOR,
+        ): BorderAnimator {
+            return BorderAnimator(
+                borderRadiusPx,
+                borderColor,
+                ScalingParams(borderWidthPx, boundsBuilder, targetView, contentView),
+                appearanceDurationMs,
+                disappearanceDurationMs,
+                interpolator,
+            )
+        }
+    }
+
+    private fun updateOutline() {
+        val interpolatedProgress = interpolator.getInterpolation(borderAnimationProgress.value)
+        borderAnimationParams.animationProgress = interpolatedProgress
+        borderPaint.alpha = (255 * interpolatedProgress).roundToInt()
+        borderPaint.strokeWidth = borderAnimationParams.borderWidth
+        borderAnimationParams.targetView.invalidate()
+    }
+
+    /**
+     * Draws the border on the given canvas.
+     *
+     * Call this method in the target view's [View.draw] method after calling super.
+     */
+    fun drawBorder(canvas: Canvas) {
+        with(borderAnimationParams) {
+            val radius = borderRadiusPx + radiusAdjustment
+            canvas.drawRoundRect(
+                /* left= */ borderBounds.left + alignmentAdjustment,
+                /* top= */ borderBounds.top + alignmentAdjustment,
+                /* right= */ borderBounds.right - alignmentAdjustment,
+                /* bottom= */ borderBounds.bottom - alignmentAdjustment,
+                /* rx= */ radius,
+                /* ry= */ radius,
+                /* paint= */ borderPaint
+            )
+        }
+    }
+
+    /** Builds the border appearance/disappearance animation. */
+    fun buildAnimator(isAppearing: Boolean): Animator {
+        return borderAnimationProgress.animateToValue(if (isAppearing) 1f else 0f).apply {
+            duration = if (isAppearing) appearanceDurationMs else disappearanceDurationMs
+            doOnStart {
+                runningBorderAnimation?.cancel()
+                runningBorderAnimation = this
+                borderAnimationParams.onShowBorder()
+            }
+            doOnEnd {
+                runningBorderAnimation = null
+                if (!isAppearing) {
+                    borderAnimationParams.onHideBorder()
+                }
+            }
+        }
+    }
+
+    /** Shows/hides the border, optionally with an animation. */
+    fun setBorderVisibility(visible: Boolean, animated: Boolean) {
+        if (animated) {
+            buildAnimator(visible).start()
+            return
+        }
+        runningBorderAnimation?.end()
+        if (visible) {
+            borderAnimationParams.onShowBorder()
+        }
+        borderAnimationProgress.updateValue(if (visible) 1f else 0f)
+        if (!visible) {
+            borderAnimationParams.onHideBorder()
+        }
+    }
+
+    /** Params for handling different target view layout situations. */
+    private abstract class BorderAnimationParams(
+        @field:Px @param:Px val borderWidthPx: Int,
+        private val boundsBuilder: (rect: Rect) -> Unit,
+        val targetView: View,
+    ) {
+        val borderBounds = Rect()
+        var animationProgress = 0f
+        private var layoutChangeListener: OnLayoutChangeListener? = null
+
+        abstract val alignmentAdjustmentInset: Int
+        abstract val radiusAdjustment: Float
+
+        val borderWidth: Float
+            get() = borderWidthPx * animationProgress
+        val alignmentAdjustment: Float
+            // Outset the border by half the width to create an outwards-growth animation
+            get() = -borderWidth / 2f + alignmentAdjustmentInset
+
+        open fun onShowBorder() {
+            if (layoutChangeListener == null) {
+                layoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+                    onShowBorder()
+                    targetView.invalidate()
+                }
+                targetView.addOnLayoutChangeListener(layoutChangeListener)
+            }
+            boundsBuilder(borderBounds)
+        }
+
+        open fun onHideBorder() {
+            if (layoutChangeListener != null) {
+                targetView.removeOnLayoutChangeListener(layoutChangeListener)
+                layoutChangeListener = null
+            }
+        }
+    }
+
+    /** BorderAnimationParams that simply draws the border outside the bounds of the target view. */
+    private class SimpleParams(
+        @Px borderWidthPx: Int,
+        boundsBuilder: (rect: Rect?) -> Unit,
+        targetView: View,
+    ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) {
+        override val alignmentAdjustmentInset = 0
+        override val radiusAdjustment: Float
+            get() = -alignmentAdjustment
+    }
+
+    /**
+     * BorderAnimationParams that scales the target and content views to draw the border within the
+     * target's bounds without obscuring the content.
+     */
+    private class ScalingParams(
+        @Px borderWidthPx: Int,
+        boundsBuilder: (rect: Rect?) -> Unit,
+        targetView: View,
+        private val contentView: View,
+    ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) {
+        // Inset the border since we are scaling the container up
+        override val alignmentAdjustmentInset = borderWidthPx
+        override val radiusAdjustment: Float
+            // Increase the radius since we are scaling the container up
+            get() = alignmentAdjustment
+
+        override fun onShowBorder() {
+            super.onShowBorder()
+            val tvWidth = targetView.width.toFloat()
+            val tvHeight = targetView.height.toFloat()
+            // Scale up just enough to make room for the border. Fail fast and fix the scaling
+            // onLayout.
+            val newScaleX = if (tvWidth == 0f) 1f else 1f + 2 * borderWidthPx / tvWidth
+            val newScaleY = if (tvHeight == 0f) 1f else 1f + 2 * borderWidthPx / tvHeight
+            with(targetView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = newScaleX
+                scaleY = newScaleY
+            }
+            with(contentView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = 1f / newScaleX
+                scaleY = 1f / newScaleY
+            }
+        }
+
+        override fun onHideBorder() {
+            super.onHideBorder()
+            with(targetView) {
+                pivotX = width.toFloat()
+                pivotY = height.toFloat()
+                scaleX = 1f
+                scaleY = 1f
+            }
+            with(contentView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = 1f
+                scaleY = 1f
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index c3774eb..689402b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -34,12 +34,12 @@
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
 import com.android.launcher3.views.BaseDragLayer
 import com.android.quickstep.views.FloatingTaskView
-import com.android.quickstep.views.IconView
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.SplitInstructionsView
 import com.android.quickstep.views.TaskThumbnailView
 import com.android.quickstep.views.TaskView
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
+import com.android.quickstep.views.TaskViewIcon
 import java.util.function.Supplier
 
 /**
@@ -82,7 +82,7 @@
                     return SplitAnimInitProps(container.thumbnailView,
                             container.thumbnailView.thumbnail, drawable!!,
                             fadeWithThumbnail = true, isStagedTask = true,
-                            iconView = container.iconView
+                            iconView = container.iconView.asView()
                     )
                 }
             }
@@ -94,7 +94,7 @@
             val drawable = getDrawable(taskView.iconView, splitSelectSource)
             return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail,
                     drawable!!, fadeWithThumbnail = true, isStagedTask = true,
-                    taskView.iconView
+                    taskView.iconView.asView()
             )
         }
     }
@@ -105,7 +105,7 @@
      * TaskView's icon drawable can be null if the TaskView is scrolled far enough off screen
      * @return [Drawable]
      */
-    fun getDrawable(iconView: IconView, splitSelectSource: SplitSelectSource?) : Drawable? {
+    fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?) : Drawable? {
         if (iconView.drawable == null && splitSelectSource != null) {
             return splitSelectSource.drawable
         }
@@ -129,7 +129,7 @@
                                 taskViewWidth: Int, taskViewHeight: Int,
                                 isPrimaryTaskSplitting: Boolean) {
         val thumbnail = taskIdAttributeContainer.thumbnailView
-        val iconView: View = taskIdAttributeContainer.iconView
+        val iconView: View = taskIdAttributeContainer.iconView.asView()
         builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f))
         thumbnail.setShowSplashForSplitSelection(true)
         if (deviceProfile.isLandscape) {
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 32d6582..dc6b5a2 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -62,6 +62,8 @@
 import java.util.List;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * TaskView that contains all tasks that are part of the desktop.
  */
@@ -142,9 +144,10 @@
     }
 
     @Override
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         bounds.set(mBackgroundView.getLeft(), mBackgroundView.getTop(), mBackgroundView.getRight(),
                 mBackgroundView.getBottom());
+        return Unit.INSTANCE;
     }
 
     @Override
@@ -327,7 +330,7 @@
     }
 
     @Override
-    protected boolean showTaskMenuWithContainer(IconView iconView) {
+    protected boolean showTaskMenuWithContainer(TaskViewIcon iconView) {
         return false;
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 7e58763..3d33c87 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -2,6 +2,7 @@
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell;
 
@@ -11,6 +12,7 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewStub;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -25,6 +27,7 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskThumbnailCache;
+import com.android.quickstep.TaskUtils;
 import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitSelectStateController;
@@ -37,6 +40,8 @@
 import java.util.HashMap;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
  *
@@ -52,7 +57,7 @@
     @Nullable
     private Task mSecondaryTask;
     private TaskThumbnailView mSnapshotView2;
-    private IconView mIconView2;
+    private TaskViewIcon mIconView2;
     @Nullable
     private CancellableTask<ThumbnailData> mThumbnailLoadRequest2;
     @Nullable
@@ -76,10 +81,10 @@
     }
 
     @Override
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         if (mSplitBoundsConfig == null) {
             super.updateBorderBounds(bounds);
-            return;
+            return Unit.INSTANCE;
         }
         bounds.set(
                 Math.min(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
@@ -90,14 +95,21 @@
                         mSnapshotView2.getRight() + Math.round(mSnapshotView2.getTranslationX())),
                 Math.max(mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()),
                         mSnapshotView2.getBottom() + Math.round(mSnapshotView2.getTranslationY())));
+        return Unit.INSTANCE;
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mSnapshotView2 = findViewById(R.id.bottomright_snapshot);
-        mIconView2 = findViewById(R.id.bottomRight_icon);
-        mIcon2TouchDelegate = new TransformingTouchDelegate(mIconView2);
+        ViewStub iconViewStub2 = findViewById(R.id.bottomRight_icon);
+        if (enableOverviewIconMenu()) {
+            iconViewStub2.setLayoutResource(R.layout.icon_app_chip_view);
+        } else {
+            iconViewStub2.setLayoutResource(R.layout.icon_view);
+        }
+        mIconView2 = (TaskViewIcon) iconViewStub2.inflate();
+        mIcon2TouchDelegate = new TransformingTouchDelegate(mIconView2.asView());
     }
 
     public void bind(Task primary, Task secondary, RecentsOrientedState orientedState,
@@ -157,6 +169,7 @@
                 mIconLoadRequest2 = iconCache.updateIconInBackground(mSecondaryTask,
                         (task) -> {
                             setIcon(mIconView2, task.icon);
+                            setText(mIconView2, TaskUtils.getTitle(getContext(), task));
                             mDigitalWellBeingToast2.initialize(mSecondaryTask);
                             mDigitalWellBeingToast2.setSplitConfiguration(mSplitBoundsConfig);
                             mDigitalWellBeingToast.setSplitConfiguration(mSplitBoundsConfig);
@@ -171,6 +184,7 @@
             }
             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
                 setIcon(mIconView2, null);
+                setText(mIconView2, null);
             }
         }
     }
@@ -302,7 +316,7 @@
         }
 
         // Check which of the two apps was selected
-        if (isCoordInView(mIconView2, mLastTouchDownPosition)
+        if (isCoordInView(mIconView2.asView(), mLastTouchDownPosition)
                 || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) {
             return 1;
         }
@@ -368,10 +382,7 @@
         super.setOrientationState(orientationState);
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         boolean isGridTask = deviceProfile.isTablet && !isFocusedTask();
-        int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
-                : deviceProfile.overviewTaskIconDrawableSizePx;
-        mIconView2.setDrawableSize(iconDrawableSize, iconDrawableSize);
-        mIconView2.setRotation(getPagedOrientationHandler().getDegreesRotated());
+        mIconView2.setIconOrientation(orientationState, isGridTask);
         updateIconPlacement();
         updateSecondaryDwbPlacement();
     }
@@ -385,7 +396,7 @@
         int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
 
-        getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2,
+        getPagedOrientationHandler().setSplitIconParams(mIconView.asView(), mIconView2.asView(),
                 taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(),
                 getMeasuredHeight(), getMeasuredWidth(), isRtl, deviceProfile,
                 mSplitBoundsConfig);
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
new file mode 100644
index 0000000..960a09e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views;
+
+import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.touch.LandscapePagedViewHandler;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.touch.SeascapePagedViewHandler;
+import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.util.RecentsOrientedState;
+
+/**
+ * An icon app menu view which can be used in place of an IconView in overview TaskViews.
+ */
+public class IconAppChipView extends FrameLayout implements TaskViewIcon {
+
+    private static final int MENU_BACKGROUND_REVEAL_DURATION = 417;
+    private static final int MENU_BACKGROUND_HIDE_DURATION = 333;
+
+    private IconView mIconView;
+    private TextView mIconTextView;
+    private ImageView mIconArrowView;
+    private ImageView mIconViewBackground;
+
+    private int mMaxIconBackgroundWidth;
+    private int mMinIconBackgroundWidth;
+    private int mMaxIconBackgroundHeight;
+    private int mMinIconBackgroundHeight;
+    private int mIconTextMinWidth;
+    private int mIconTextMaxWidth;
+    private int mInnerMargin;
+    private int mIconArrowSize;
+    private int mIconMenuMarginStart;
+    private int mArrowMaxTranslationX;
+
+    public IconAppChipView(Context context) {
+        this(context, null);
+    }
+
+    public IconAppChipView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mMaxIconBackgroundWidth = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_max_width);
+        mMinIconBackgroundWidth = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_min_width);
+        mMaxIconBackgroundHeight = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_max_height);
+        mMinIconBackgroundHeight = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_min_height);
+        mIconTextMaxWidth = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_text_max_width);
+        mInnerMargin = (int) getResources().getDimension(
+                R.dimen.task_thumbnail_icon_menu_arrow_margin);
+        mIconTextMinWidth = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_text_width) + (2 * mInnerMargin);
+        int taskIconHeight = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_drawable_touch_size);
+        int arrowWidth = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_arrow_size);
+        mIconArrowSize = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_arrow_drawable_size);
+        mIconMenuMarginStart = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_margin);
+        mArrowMaxTranslationX =
+                mMaxIconBackgroundWidth - taskIconHeight - mIconTextMaxWidth + arrowWidth;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mIconView = findViewById(R.id.icon_view);
+        mIconTextView = findViewById(R.id.icon_text);
+        mIconArrowView = findViewById(R.id.icon_arrow);
+        mIconViewBackground = findViewById(R.id.icon_view_background);
+    }
+
+    protected IconView getIconView() {
+        return mIconView;
+    }
+
+    @Override
+    public void setText(CharSequence text) {
+        if (mIconTextView != null) {
+            mIconTextView.setText(text);
+        }
+    }
+
+    @Override
+    public Drawable getDrawable() {
+        return mIconView == null ? null : mIconView.getDrawable();
+    }
+
+    @Override
+    public void setDrawable(Drawable icon) {
+        if (mIconView != null) {
+            mIconView.setDrawable(icon);
+        }
+    }
+
+    @Override
+    public void setDrawableSize(int iconWidth, int iconHeight) {
+        if (mIconView != null) {
+            mIconView.setDrawableSize(iconWidth, iconHeight);
+        }
+    }
+
+    @Override
+    public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
+
+        PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
+        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        DeviceProfile deviceProfile =
+                ActivityContext.lookupContext(getContext()).getDeviceProfile();
+
+        int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
+        int taskIconSize = deviceProfile.overviewTaskIconSizePx;
+        int taskMargin = deviceProfile.overviewTaskMarginPx;
+
+        LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
+        orientationHandler.setTaskIconMenuParams(iconMenuParams, mIconMenuMarginStart,
+                thumbnailTopMargin);
+        iconMenuParams.width = mMinIconBackgroundWidth;
+        iconMenuParams.height = taskIconSize;
+        if (orientationHandler instanceof SeascapePagedViewHandler) {
+            // Use half menu height to place the pivot within the X/Y center of icon in the menu.
+            setPivotX(getHeight() / 2f);
+            setPivotY(getHeight() / 2f - mIconMenuMarginStart);
+        } else if (orientationHandler instanceof LandscapePagedViewHandler) {
+            setPivotX(getWidth());
+            setPivotY(0);
+        }
+        // Pivot not updated for PortraitPagedViewHandler case, as it has 0 rotation.
+
+        setTranslationY(0);
+        setRotation(orientationHandler.getDegreesRotated());
+
+        LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
+        orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconSize,
+                thumbnailTopMargin, isRtl);
+        iconParams.width = iconParams.height = taskIconSize;
+        iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
+        mIconView.setLayoutParams(iconParams);
+
+        int iconDrawableSize = enableOverviewIconMenu()
+                ? deviceProfile.overviewTaskIconAppChipMenuDrawableSizePx
+                : isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
+                        : deviceProfile.overviewTaskIconDrawableSizePx;
+        mIconView.setDrawableSize(iconDrawableSize, iconDrawableSize);
+
+        LayoutParams iconTextParams = (LayoutParams) mIconTextView.getLayoutParams();
+        orientationHandler.setTaskIconParams(iconTextParams, 0, taskIconSize,
+                thumbnailTopMargin, isRtl);
+        iconTextParams.width = mIconTextMaxWidth;
+        iconTextParams.height = taskIconSize;
+        iconTextParams.setMarginStart(taskIconSize);
+        iconTextParams.topMargin = (getHeight() - mIconTextView.getHeight()) / 2;
+        iconTextParams.gravity = Gravity.CENTER_VERTICAL | Gravity.START;
+        mIconTextView.setLayoutParams(iconTextParams);
+        mIconTextView.setRevealClip(true, 0, taskIconSize / 2f, mIconTextMinWidth);
+
+        LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
+        iconArrowParams.gravity = Gravity.CENTER_VERTICAL | Gravity.END;
+        iconArrowParams.setMarginStart(taskIconSize + mIconTextMinWidth);
+        iconArrowParams.setMarginEnd(mInnerMargin);
+        mIconArrowView.setLayoutParams(iconArrowParams);
+        mIconArrowView.getDrawable().setBounds(0, 0, mIconArrowSize, mIconArrowSize);
+
+        LayoutParams backgroundParams = (LayoutParams) mIconViewBackground.getLayoutParams();
+        backgroundParams.width = mMinIconBackgroundWidth;
+        backgroundParams.height = taskIconSize;
+        mIconViewBackground.setPivotX(
+                isRtl ? mMinIconBackgroundWidth - (taskIconSize / 2f - mInnerMargin)
+                        : taskIconSize / 2f - mInnerMargin);
+        mIconViewBackground.setPivotY(taskIconSize / 2f);
+
+        requestLayout();
+    }
+
+    @Override
+    public void setIconColorTint(int color, float amount) {
+        if (mIconView != null) {
+            mIconView.setIconColorTint(color, amount);
+        }
+    }
+
+    @Override
+    public int getDrawableWidth() {
+        return mIconView == null ? 0 : mIconView.getDrawableWidth();
+    }
+
+    @Override
+    public int getDrawableHeight() {
+        return mIconView == null ? 0 : mIconView.getDrawableHeight();
+    }
+
+    protected void revealAnim(boolean isRevealing) {
+        if (isRevealing) {
+            ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).start();
+            AnimatorSet anim = new AnimatorSet();
+            anim.playTogether(
+                    ViewAnimationUtils.createCircularReveal(mIconTextView, 0,
+                            mIconTextView.getHeight() / 2, mIconTextMinWidth, mIconTextMaxWidth),
+                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_X,
+                            mMaxIconBackgroundWidth / (float) mMinIconBackgroundWidth),
+                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_Y,
+                            mMaxIconBackgroundHeight / (float) mMinIconBackgroundHeight),
+                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X,
+                            isLayoutRtl() ? -mArrowMaxTranslationX : mArrowMaxTranslationX));
+            anim.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
+            anim.setInterpolator(EMPHASIZED);
+            anim.start();
+        } else {
+            ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reverse();
+            AnimatorSet anim = new AnimatorSet();
+            Animator textRevealAnim = ViewAnimationUtils.createCircularReveal(mIconTextView, 0,
+                    mIconTextView.getHeight() / 2, mIconTextMaxWidth, mIconTextMinWidth);
+            textRevealAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    // createCircularReveal removes clip on finish, restore it here to clip text.
+                    mIconTextView.setRevealClip(true, 0, mIconTextView.getHeight() / 2f,
+                            mIconTextMinWidth);
+                }
+            });
+            anim.playTogether(
+                    textRevealAnim,
+                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_X, 1),
+                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_Y, 1),
+                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0));
+            anim.setDuration(MENU_BACKGROUND_HIDE_DURATION);
+            anim.setInterpolator(EMPHASIZED);
+            anim.start();
+        }
+    }
+
+    @Override
+    public View asView() {
+        return this;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
index 5895c05..222f9ca 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
@@ -22,16 +24,21 @@
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
+import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.util.RecentsOrientedState;
 
 /**
  * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
  * when the drawable changes.
  */
-public class IconView extends View {
+public class IconView extends View implements TaskViewIcon {
 
     @Nullable
     private Drawable mDrawable;
@@ -52,6 +59,7 @@
     /**
      * Sets a {@link Drawable} to be displayed.
      */
+    @Override
     public void setDrawable(@Nullable Drawable d) {
         if (mDrawable != null) {
             mDrawable.setCallback(null);
@@ -67,6 +75,7 @@
     /**
      * Sets the size of the icon drawable.
      */
+    @Override
     public void setDrawableSize(int iconWidth, int iconHeight) {
         mDrawableWidth = iconWidth;
         mDrawableHeight = iconHeight;
@@ -82,15 +91,18 @@
         mDrawable.setBounds(drawableRect);
     }
 
+    @Override
     @Nullable
     public Drawable getDrawable() {
         return mDrawable;
     }
 
+    @Override
     public int getDrawableWidth() {
         return mDrawableWidth;
     }
 
+    @Override
     public int getDrawableHeight() {
         return mDrawableHeight;
     }
@@ -147,9 +159,41 @@
      * @param color to blend in.
      * @param amount [0,1] 0 no tint, 1 full tint
      */
+    @Override
     public void setIconColorTint(int color, float amount) {
         if (mDrawable != null) {
             mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
         }
     }
+
+    @Override
+    public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
+        PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
+        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        DeviceProfile deviceProfile =
+                ActivityContext.lookupContext(getContext()).getDeviceProfile();
+
+        FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
+
+        int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
+        int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
+        int taskMargin = deviceProfile.overviewTaskMarginPx;
+
+        orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
+                thumbnailTopMargin, isRtl);
+        iconParams.width = iconParams.height = taskIconHeight;
+        setLayoutParams(iconParams);
+
+        setRotation(orientationHandler.getDegreesRotated());
+        int iconDrawableSize = enableOverviewIconMenu()
+                ? deviceProfile.overviewTaskIconAppChipMenuDrawableSizePx
+                : isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
+                        : deviceProfile.overviewTaskIconDrawableSizePx;
+        setDrawableSize(iconDrawableSize, iconDrawableSize);
+    }
+
+    @Override
+    public View asView() {
+        return this;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index b4d24e5..62c0bef 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.views;
 
+import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
 import android.animation.Animator;
@@ -24,6 +26,7 @@
 import android.content.Context;
 import android.graphics.Outline;
 import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.ShapeDrawable;
 import android.graphics.drawable.shapes.RectShape;
 import android.util.AttributeSet;
@@ -58,16 +61,19 @@
 
     private static final Rect sTempRect = new Rect();
 
-    private static final int REVEAL_OPEN_DURATION = 150;
-    private static final int REVEAL_CLOSE_DURATION = 100;
+    private static final int REVEAL_OPEN_DURATION = enableOverviewIconMenu() ? 417 : 150;
+    private static final int REVEAL_CLOSE_DURATION = enableOverviewIconMenu() ? 333 : 100;
 
     private BaseDraggingActivity mActivity;
     private TextView mTaskName;
     @Nullable
     private AnimatorSet mOpenCloseAnimator;
+    @Nullable private Runnable mOnClosingStartCallback;
     private TaskView mTaskView;
     private TaskIdAttributeContainer mTaskContainer;
     private LinearLayout mOptionLayout;
+    private float mMenuTranslationYBeforeOpen;
+    private float mIconViewTranslationYBeforeOpen;
 
     public TaskMenuView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -137,14 +143,20 @@
         }
     }
 
-    public static boolean showForTask(TaskIdAttributeContainer taskContainer) {
+    public static boolean showForTask(TaskIdAttributeContainer taskContainer,
+            @Nullable Runnable onClosingStartCallback) {
         BaseDraggingActivity activity = BaseDraggingActivity.fromContext(
                 taskContainer.getTaskView().getContext());
         final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate(
                         R.layout.task_menu, activity.getDragLayer(), false);
+        taskMenuView.setOnClosingStartCallback(onClosingStartCallback);
         return taskMenuView.populateAndShowForTask(taskContainer);
     }
 
+    public static boolean showForTask(TaskIdAttributeContainer taskContainer) {
+        return showForTask(taskContainer, null);
+    }
+
     private boolean populateAndShowForTask(TaskIdAttributeContainer taskContainer) {
         if (isAttachedToWindow()) {
             return false;
@@ -171,8 +183,12 @@
     }
 
     private void addMenuOptions(TaskIdAttributeContainer taskContainer) {
-        mTaskName.setText(TaskUtils.getTitle(getContext(), taskContainer.getTask()));
-        mTaskName.setOnClickListener(v -> close(true));
+        if (enableOverviewIconMenu()) {
+            removeView(mTaskName);
+        } else {
+            mTaskName.setText(TaskUtils.getTitle(getContext(), taskContainer.getTask()));
+            mTaskName.setOnClickListener(v -> close(true));
+        }
         TaskOverlayFactory.getEnabledShortcuts(mTaskView, taskContainer)
                 .forEach(this::addMenuOption);
     }
@@ -180,6 +196,9 @@
     private void addMenuOption(SystemShortcut menuOption) {
         LinearLayout menuOptionView = (LinearLayout) mActivity.getLayoutInflater().inflate(
                 R.layout.task_view_menu_option, this, false);
+        if (enableOverviewIconMenu()) {
+            ((GradientDrawable) menuOptionView.getBackground()).setCornerRadius(0);
+        }
         menuOption.setIconAndLabelFor(
                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
         LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
@@ -198,8 +217,9 @@
 
         // Get Position
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-        mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskContainer.getThumbnailView(),
-                sTempRect);
+        mActivity.getDragLayer().getDescendantRectRelativeToSelf(
+                enableOverviewIconMenu() ? taskContainer.getIconView().asView()
+                        : taskContainer.getThumbnailView(), sTempRect);
         Rect insets = mActivity.getDragLayer().getInsets();
         BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
         int padding = getResources()
@@ -217,12 +237,17 @@
         ShapeDrawable divider = new ShapeDrawable(new RectShape());
         divider.getPaint().setColor(getResources().getColor(android.R.color.transparent));
         int dividerSpacing = (int) getResources().getDimension(R.dimen.task_menu_spacing);
-        mOptionLayout.setShowDividers(SHOW_DIVIDER_MIDDLE);
+        mOptionLayout.setShowDividers(
+                enableOverviewIconMenu() ? SHOW_DIVIDER_NONE : SHOW_DIVIDER_MIDDLE);
 
         orientationHandler.setTaskOptionsMenuLayoutOrientation(
                 deviceProfile, mOptionLayout, dividerSpacing, divider);
-        float thumbnailAlignedX = sTempRect.left - insets.left;
-        float thumbnailAlignedY = sTempRect.top - insets.top;
+        float thumbnailAlignedX = sTempRect.left - insets.left + (enableOverviewIconMenu()
+                ? -getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_touch_max_margin) : 0);
+        float thumbnailAlignedY = sTempRect.top - insets.top + (enableOverviewIconMenu()
+                ? getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_max_height)
+                - 2 * dividerSpacing : 0);
         // Changing pivot to make computations easier
         // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
         // which would render the X and Y position set here incorrect
@@ -231,15 +256,22 @@
         setRotation(orientationHandler.getDegreesRotated());
 
         // Margin that insets the menuView inside the taskView
-        float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
+        float taskInsetMargin =
+                enableOverviewIconMenu() ? getResources().getDimension(
+                        R.dimen.task_thumbnail_icon_menu_margin) : getResources().getDimension(
+                        R.dimen.task_card_margin);
         setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
-                mTaskContainer.getThumbnailView(), deviceProfile, taskInsetMargin));
+                mTaskContainer.getThumbnailView(), deviceProfile, taskInsetMargin,
+                mTaskContainer.getIconView().asView()));
         setTranslationY(orientationHandler.getTaskMenuY(
                 thumbnailAlignedY, mTaskContainer.getThumbnailView(),
-                mTaskContainer.getStagePosition(), this, taskInsetMargin));
+                mTaskContainer.getStagePosition(), this, taskInsetMargin,
+                mTaskContainer.getIconView().asView()));
     }
 
     private void animateOpen() {
+        mMenuTranslationYBeforeOpen = getTranslationY();
+        mIconViewTranslationYBeforeOpen = mTaskContainer.getIconView().asView().getTranslationY();
         animateOpenOrClosed(false);
         mIsOpen = true;
     }
@@ -256,7 +288,29 @@
 
         final Animator revealAnimator = createOpenCloseOutlineProvider()
                 .createRevealAnimator(this, closing);
-        revealAnimator.setInterpolator(Interpolators.DECELERATE);
+        revealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
+                : Interpolators.DECELERATE);
+
+        if (enableOverviewIconMenu()
+                && ((RecentsView) mActivity.getOverviewPanel()).isOnGridBottomRow(mTaskView)) {
+            float taskBottom = mTaskView.getHeight() + mTaskView.getPersistentTranslationY();
+            float menuBottom = getHeight() + mMenuTranslationYBeforeOpen;
+            float additionalTranslationY = Math.max(menuBottom - taskBottom, 0);
+
+            ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(this, TRANSLATION_Y,
+                    closing ? mMenuTranslationYBeforeOpen
+                            : mMenuTranslationYBeforeOpen - additionalTranslationY);
+            translationYAnim.setInterpolator(EMPHASIZED);
+
+            ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
+                    mTaskContainer.getIconView().asView(), TRANSLATION_Y,
+                    closing ? mIconViewTranslationYBeforeOpen
+                            : mIconViewTranslationYBeforeOpen - additionalTranslationY);
+            menuTranslationYAnim.setInterpolator(EMPHASIZED);
+
+            mOpenCloseAnimator.playTogether(translationYAnim, menuTranslationYAnim);
+        }
+
         mOpenCloseAnimator.playTogether(revealAnimator,
                 ObjectAnimator.ofFloat(
                         mTaskContainer.getThumbnailView(), DIM_ALPHA,
@@ -266,6 +320,9 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 setVisibility(VISIBLE);
+                if (closing && mOnClosingStartCallback != null) {
+                    mOnClosingStartCallback.run();
+                }
             }
 
             @Override
@@ -286,9 +343,16 @@
 
     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
         float radius = TaskCornerRadius.get(mContext);
-        Rect fromRect = new Rect(0, 0, getWidth(), 0);
+        Rect fromRect = new Rect(
+                enableOverviewIconMenu() && isLayoutRtl() ? getWidth() : 0,
+                0,
+                enableOverviewIconMenu() && !isLayoutRtl() ? 0 : getWidth(),
+                0);
         Rect toRect = new Rect(0, 0, getWidth(), getHeight());
         return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
     }
 
+    private void setOnClosingStartCallback(Runnable onClosingStartCallback) {
+        mOnClosingStartCallback = onClosingStartCallback;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index b373911..12b8b6f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -106,7 +106,7 @@
     override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0
 
     override fun getTargetObjectLocation(outPos: Rect?) {
-        popupContainer.getDescendantRectRelativeToSelf(taskContainer.iconView, outPos)
+        popupContainer.getDescendantRectRelativeToSelf(taskContainer.iconView.asView(), outPos)
     }
 
     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index d5b43a8..6ae1973 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -25,6 +25,7 @@
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -63,6 +64,7 @@
 import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
@@ -120,6 +122,8 @@
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
+import kotlin.Unit;
+
 /**
  * A task in the Recents view.
  */
@@ -350,7 +354,7 @@
     @Nullable
     protected Task mTask;
     protected TaskThumbnailView mSnapshotView;
-    protected IconView mIconView;
+    protected TaskViewIcon mIconView;
     protected final DigitalWellBeingToast mDigitalWellBeingToast;
     protected float mFullscreenProgress;
     private float mGridProgress;
@@ -440,48 +444,44 @@
 
         boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
                 || DesktopTaskView.DESKTOP_MODE_SUPPORTED;
+        boolean cursorHoverStatesEnabled = FeatureFlags.enableCursorHoverStates();
 
-        boolean willDrawBorder =
-                keyboardFocusHighlightEnabled || FeatureFlags.enableCursorHoverStates();
-        setWillNotDraw(!willDrawBorder);
+        setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled);
 
-        if (willDrawBorder) {
-            TypedArray styledAttrs = context.obtainStyledAttributes(
-                    attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
+        TypedArray styledAttrs = context.obtainStyledAttributes(
+                attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
 
-            mFocusBorderAnimator = keyboardFocusHighlightEnabled ? new BorderAnimator(
-                    /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                    /* borderColor= */ styledAttrs.getColor(
-                            R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR),
-                    /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                            /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
-                                    R.dimen.keyboard_quick_switch_border_width),
-                            /* boundsBuilder= */ this::updateBorderBounds,
-                            /* targetView= */ this)) : null;
+        mFocusBorderAnimator = keyboardFocusHighlightEnabled
+                ? BorderAnimator.createSimpleBorderAnimator(
+                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                        /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                R.dimen.keyboard_quick_switch_border_width),
+                        /* boundsBuilder= */ this::updateBorderBounds,
+                        /* targetView= */ this,
+                        /* borderColor= */ styledAttrs.getColor(
+                                R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR))
+                : null;
 
-            mHoverBorderAnimator =
-                    FeatureFlags.enableCursorHoverStates() ? new BorderAnimator(
-                            /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                            /* borderColor= */ styledAttrs.getColor(
-                                    R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR),
-                            /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                                    /* borderWidthPx= */ context.getResources()
-                                            .getDimensionPixelSize(R.dimen.task_hover_border_width),
-                                    /* boundsBuilder= */ this::updateBorderBounds,
-                                    /* targetView= */ this)) : null;
+        mHoverBorderAnimator = cursorHoverStatesEnabled
+                ? BorderAnimator.createSimpleBorderAnimator(
+                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                        /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                R.dimen.task_hover_border_width),
+                        /* boundsBuilder= */ this::updateBorderBounds,
+                        /* targetView= */ this,
+                        /* borderColor= */ styledAttrs.getColor(
+                                R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR))
+                : null;
 
-            styledAttrs.recycle();
-        } else {
-            mFocusBorderAnimator = null;
-            mHoverBorderAnimator = null;
-        }
+        styledAttrs.recycle();
     }
 
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         bounds.set(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
                 mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()),
                 mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()),
                 mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()));
+        return Unit.INSTANCE;
     }
 
     public void setTaskViewId(int id) {
@@ -521,27 +521,35 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mSnapshotView = findViewById(R.id.snapshot);
-        mIconView = findViewById(R.id.icon);
-        mIconTouchDelegate = new TransformingTouchDelegate(mIconView);
+        ViewStub iconViewStub = findViewById(R.id.icon);
+        if (enableOverviewIconMenu()) {
+            iconViewStub.setLayoutResource(R.layout.icon_app_chip_view);
+        } else {
+            iconViewStub.setLayoutResource(R.layout.icon_view);
+        }
+        mIconView = (TaskViewIcon) iconViewStub.inflate();
+        mIconTouchDelegate = new TransformingTouchDelegate(mIconView.asView());
     }
 
     @Override
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
         if (mFocusBorderAnimator != null) {
-            mFocusBorderAnimator.buildAnimator(gainFocus).start();
+            mFocusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true);
         }
     }
 
     @Override
     public boolean onHoverEvent(MotionEvent event) {
-        if (FeatureFlags.enableCursorHoverStates()) {
+        if (mHoverBorderAnimator != null) {
             switch (event.getAction()) {
                 case MotionEvent.ACTION_HOVER_ENTER:
-                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ true).start();
+                    mHoverBorderAnimator.setBorderVisibility(
+                            /* visible= */ true, /* animated= */ true);
                     break;
                 case MotionEvent.ACTION_HOVER_EXIT:
-                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ false).start();
+                    mHoverBorderAnimator.setBorderVisibility(
+                            /* visible= */ false, /* animated= */ true);
                     break;
                 default:
                     break;
@@ -563,14 +571,14 @@
 
     @Override
     public void draw(Canvas canvas) {
-        super.draw(canvas);
+        // Draw border first so any child views outside of the thumbnail bounds are drawn above it.
         if (mFocusBorderAnimator != null) {
             mFocusBorderAnimator.drawBorder(canvas);
         }
-
         if (mHoverBorderAnimator != null) {
             mHoverBorderAnimator.drawBorder(canvas);
         }
+        super.draw(canvas);
     }
 
     /**
@@ -587,17 +595,22 @@
         return false;
     }
 
-    protected void computeAndSetIconTouchDelegate(IconView iconView, float[] tempCenterCoords,
+    protected void computeAndSetIconTouchDelegate(TaskViewIcon view, float[] tempCenterCoords,
             TransformingTouchDelegate transformingTouchDelegate) {
-        float iconHalfSize = iconView.getWidth() / 2f;
-        tempCenterCoords[0] = tempCenterCoords[1] = iconHalfSize;
-        getDescendantCoordRelativeToAncestor(iconView, mActivity.getDragLayer(), tempCenterCoords,
-                false);
+        if (view == null) {
+            return;
+        }
+        float viewHalfWidth = view.getWidth() / 2f;
+        float viewHalfHeight = view.getHeight() / 2f;
+        tempCenterCoords[0] = viewHalfWidth;
+        tempCenterCoords[1] = viewHalfHeight;
+        getDescendantCoordRelativeToAncestor(view.asView(), mActivity.getDragLayer(),
+                tempCenterCoords, false);
         transformingTouchDelegate.setBounds(
-                (int) (tempCenterCoords[0] - iconHalfSize),
-                (int) (tempCenterCoords[1] - iconHalfSize),
-                (int) (tempCenterCoords[0] + iconHalfSize),
-                (int) (tempCenterCoords[1] + iconHalfSize));
+                (int) (tempCenterCoords[0] - viewHalfWidth),
+                (int) (tempCenterCoords[1] - viewHalfHeight),
+                (int) (tempCenterCoords[0] + viewHalfWidth),
+                (int) (tempCenterCoords[1] + viewHalfHeight));
     }
 
     /**
@@ -629,8 +642,8 @@
         cancelPendingLoadTasks();
         mTask = task;
         mTaskIdContainer[0] = mTask.key.id;
-        mTaskIdAttributeContainer[0] = new TaskIdAttributeContainer(task, mSnapshotView,
-                mIconView, STAGE_POSITION_UNDEFINED);
+        mTaskIdAttributeContainer[0] = new TaskIdAttributeContainer(task, mSnapshotView, mIconView,
+                STAGE_POSITION_UNDEFINED);
         mSnapshotView.bind(task);
         setOrientationState(orientedState);
     }
@@ -745,7 +758,7 @@
         return new TaskThumbnailView[]{mSnapshotView};
     }
 
-    public IconView getIconView() {
+    public TaskViewIcon getIconView() {
         return mIconView;
     }
 
@@ -1075,6 +1088,7 @@
                 mIconLoadRequest = iconCache.updateIconInBackground(mTask,
                         (task) -> {
                             setIcon(mIconView, task.icon);
+                            setText(mIconView, TaskUtils.getTitle(getContext(), task));
                             mDigitalWellBeingToast.initialize(task);
                         });
             }
@@ -1090,6 +1104,7 @@
             }
             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
                 setIcon(mIconView, null);
+                setText(mIconView, null);
             }
         }
     }
@@ -1109,7 +1124,7 @@
         }
     }
 
-    private boolean showTaskMenu(IconView iconView) {
+    private boolean showTaskMenu(TaskViewIcon iconView) {
         if (!getRecentsView().canLaunchFullscreenTask()) {
             // Don't show menu when selecting second split screen app
             return true;
@@ -1126,11 +1141,16 @@
         }
     }
 
-    protected boolean showTaskMenuWithContainer(IconView iconView) {
+    protected boolean showTaskMenuWithContainer(TaskViewIcon iconView) {
         TaskIdAttributeContainer menuContainer =
                 mTaskIdAttributeContainer[iconView == mIconView ? 0 : 1];
         DeviceProfile dp = mActivity.getDeviceProfile();
-        if (dp.isTablet) {
+        if (enableOverviewIconMenu() && iconView instanceof IconAppChipView) {
+            ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ true);
+            return TaskMenuView.showForTask(menuContainer, () -> {
+                ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false);
+            });
+        } else if (dp.isTablet) {
             int alignedOptionIndex = 0;
             if (getRecentsView().isOnGridBottomRow(menuContainer.getTaskView()) && dp.isLandscape) {
                 if (FeatureFlags.enableGridOnlyOverview()) {
@@ -1142,13 +1162,14 @@
                     alignedOptionIndex = 1;
                 }
             }
-            return TaskMenuViewWithArrow.Companion.showForTask(menuContainer, alignedOptionIndex);
+            return TaskMenuViewWithArrow.Companion.showForTask(menuContainer,
+                    alignedOptionIndex);
         } else {
             return TaskMenuView.showForTask(menuContainer);
         }
     }
 
-    protected void setIcon(IconView iconView, @Nullable Drawable icon) {
+    protected void setIcon(TaskViewIcon iconView, @Nullable Drawable icon) {
         if (icon != null) {
             iconView.setDrawable(icon);
             iconView.setOnClickListener(v -> {
@@ -1168,32 +1189,13 @@
         }
     }
 
-    public void setOrientationState(RecentsOrientedState orientationState) {
-        setIconOrientation(orientationState);
-        setThumbnailOrientation(orientationState);
+    protected void setText(TaskViewIcon iconView, CharSequence text) {
+        iconView.setText(text);
     }
 
-    protected void setIconOrientation(RecentsOrientedState orientationState) {
-        PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
-        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-
-        boolean isGridTask = isGridTask();
-        LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
-
-        int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
-        int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
-        int taskMargin = deviceProfile.overviewTaskMarginPx;
-
-        orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
-                thumbnailTopMargin, isRtl);
-        iconParams.width = iconParams.height = taskIconHeight;
-        mIconView.setLayoutParams(iconParams);
-
-        mIconView.setRotation(orientationHandler.getDegreesRotated());
-        int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
-                : deviceProfile.overviewTaskIconDrawableSizePx;
-        mIconView.setDrawableSize(iconDrawableSize, iconDrawableSize);
+    public void setOrientationState(RecentsOrientedState orientationState) {
+        mIconView.setIconOrientation(orientationState, isGridTask());
+        setThumbnailOrientation(orientationState);
     }
 
     protected void setThumbnailOrientation(RecentsOrientedState orientationState) {
@@ -1898,14 +1900,14 @@
     public class TaskIdAttributeContainer {
         private final TaskThumbnailView mThumbnailView;
         private final Task mTask;
-        private final IconView mIconView;
+        private final TaskViewIcon mIconView;
         /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */
         private @SplitConfigurationOptions.StagePosition int mStagePosition;
         @IdRes
         private final int mA11yNodeId;
 
         public TaskIdAttributeContainer(Task task, TaskThumbnailView thumbnailView,
-                IconView iconView, int stagePosition) {
+                TaskViewIcon iconView, int stagePosition) {
             this.mTask = task;
             this.mThumbnailView = thumbnailView;
             this.mIconView = iconView;
@@ -1930,7 +1932,7 @@
             return TaskView.this;
         }
 
-        public IconView getIconView() {
+        public TaskViewIcon getIconView() {
             return mIconView;
         }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskViewIcon.java b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java
new file mode 100644
index 0000000..b4f21be
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views;
+
+import android.annotation.Nullable;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.quickstep.util.RecentsOrientedState;
+
+/**
+ * Interface defining an object which can be used as a TaskView's icon.
+ */
+public interface TaskViewIcon {
+
+    /**
+     * Returns the width of this icon view.
+     */
+    int getWidth();
+
+    /**
+     * Returns the height of this icon view.
+     */
+    int getHeight();
+
+    /**
+     * Sets the opacity of the view.
+     */
+    void setAlpha(float alpha);
+
+    /**
+     * Returns this icon view's drawable.
+     */
+    @Nullable Drawable getDrawable();
+
+    /**
+     * Sets a {@link Drawable} to be displayed.
+     */
+    void setDrawable(@Nullable Drawable icon);
+
+    /**
+     * Register a callback to be invoked when this view is clicked.
+     */
+    void setOnClickListener(@Nullable View.OnClickListener l);
+
+    /**
+     * Register a callback to be invoked when this view is clicked and held.
+     */
+    void setOnLongClickListener(@Nullable View.OnLongClickListener l);
+
+    /**
+     * Returns the LayoutParams associated with this view.
+     */
+    ViewGroup.LayoutParams getLayoutParams();
+
+    /**
+     * Sets the layout parameters associated with this view.
+     */
+    void setLayoutParams(ViewGroup.LayoutParams params);
+
+    /**
+     * Sets the degrees that the view is rotated around the pivot point.
+     */
+    void setRotation(float rotation);
+
+    /**
+     * Sets the size of the icon drawable.
+     */
+    void setDrawableSize(int iconWidth, int iconHeight);
+
+    /**
+     * Sets the orientation of this icon view based on the provided orientationState.
+     */
+    void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask);
+
+    /**
+     * Sets the visibility state of this view.
+     */
+    void setVisibility(int visibility);
+
+    /**
+     * Sets the tint color of the icon, useful for scrimming or dimming.
+     *
+     * @param color to blend in.
+     * @param amount [0,1] 0 no tint, 1 full tint
+     */
+    void setIconColorTint(int color, float amount);
+
+    /**
+     * Gets the opacity of the view.
+     */
+    float getAlpha();
+
+    /**
+     * Returns the width of this icon view's drawable.
+     */
+    int getDrawableWidth();
+
+    /**
+     * Returns the height of this icon view's drawable.
+     */
+    int getDrawableHeight();
+
+    /**
+     * Directly calls any attached OnClickListener.
+     */
+    boolean callOnClick();
+
+    /**
+     * Calls this view's OnLongClickListener.
+     */
+    boolean performLongClick();
+
+    /**
+     * Sets the text for this icon view if any text view is associated.
+     */
+    default void setText(CharSequence text) {}
+
+    /**
+     * Returns this icon view cast as a View.
+     */
+    View asView();
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
index 3920b08..16bfe70 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
@@ -13,6 +13,7 @@
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarManager
+import com.android.systemui.shared.rotation.RotationButton
 import java.lang.IllegalStateException
 import org.junit.Assume.assumeTrue
 import org.junit.Before
@@ -34,6 +35,9 @@
     @Mock lateinit var mockBackButton: ImageView
     @Mock lateinit var mockRecentsButton: ImageView
     @Mock lateinit var mockHomeButton: ImageView
+    @Mock lateinit var mockImeSwitcher: ImageView
+    @Mock lateinit var mockRotationButton: RotationButton
+    @Mock lateinit var mockA11yButton: ImageView
 
     private var surfaceRotation = Surface.ROTATION_0
 
@@ -196,7 +200,10 @@
             isInSetup = isInSetup,
             isThreeButtonNav = isThreeButtonNav,
             phoneMode = phoneMode,
-            surfaceRotation = surfaceRotation
+            surfaceRotation = surfaceRotation,
+            imeSwitcher = mockImeSwitcher,
+            rotationButton = mockRotationButton,
+            a11yButton = mockA11yButton
         )
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 7d82944..99c79b3 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -32,8 +32,6 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.rule.ShellCommandRule.disableHeadsUpNotification;
 import static com.android.launcher3.util.rule.ShellCommandRule.getLauncherCommand;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -61,6 +59,7 @@
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
+import com.android.launcher3.util.rule.TestIsolationRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.rule.ViewCaptureRule;
 import com.android.quickstep.views.RecentsView;
@@ -94,9 +93,6 @@
     public final TestRule mDisableHeadsUpNotification = disableHeadsUpNotification();
 
     @Rule
-    public final TestRule mSetLauncherCommand;
-
-    @Rule
     public final TestRule mOrderSensitiveRules;
 
     @Rule
@@ -116,19 +112,7 @@
             Utilities.enableRunningInTestHarnessForTests();
         }
 
-        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
-                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
-        mOrderSensitiveRules = RuleChain
-                .outerRule(new SamplerRule())
-                .around(new NavigationModeSwitchRule(mLauncher))
-                .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
-                .around(viewCaptureRule);
-
-        mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
-                getHomeIntentInPackage(context),
-                MATCH_DISABLED_COMPONENTS).get(0).activityInfo;
-
-        mSetLauncherCommand = (base, desc) -> new Statement() {
+        final TestRule setLauncherCommand = (base, desc) -> new Statement() {
             @Override
             public void evaluate() throws Throwable {
                 TestCommandReceiver.callCommand(TestCommandReceiver.ENABLE_TEST_LAUNCHER);
@@ -152,6 +136,21 @@
             }
         };
 
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
+                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
+        mOrderSensitiveRules = RuleChain
+                .outerRule(new SamplerRule())
+                .around(new TestStabilityRule())
+                .around(new NavigationModeSwitchRule(mLauncher))
+                .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
+                .around(viewCaptureRule)
+                .around(new TestIsolationRule(mLauncher, false))
+                .around(setLauncherCommand);
+
+        mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
+                getHomeIntentInPackage(context),
+                MATCH_DISABLED_COMPONENTS).get(0).activityInfo;
+
         if (TestHelpers.isInLauncherProcess()) {
             mLauncher.setSystemHealthSupplier(startTime -> TestCommandReceiver.callCommand(
                     TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()).
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
new file mode 100644
index 0000000..74f37a4
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.tapl.KeyboardQuickSwitch;
+import com.android.launcher3.ui.TaplTestsLauncher3;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest {
+
+    private enum TestSurface {
+        HOME, LAUNCHED_APP, HOME_ALL_APPS, WIDGETS,
+    }
+
+    private enum TestCase {
+        DISMISS(0),
+        LAUNCH_LAST_APP(0),
+        LAUNCH_SELECTED_APP(1),
+        LAUNCH_OVERVIEW(5);
+
+        private final int mNumAdditionalRunningTasks;
+
+        TestCase(int numAdditionalRunningTasks) {
+            mNumAdditionalRunningTasks = numAdditionalRunningTasks;
+        }
+    }
+
+    private static final String CALCULATOR_APP_PACKAGE =
+            resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+
+    @Override
+    public void setUp() throws Exception {
+        Assume.assumeTrue(mLauncher.isTablet());
+        super.setUp();
+        TaplTestsLauncher3.initialize(this);
+        startAppFast(CALCULATOR_APP_PACKAGE);
+        startTestActivity(2);
+    }
+
+    @Test
+    public void testDismiss_fromHome() {
+        runTest(TestSurface.HOME, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testDismiss_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testDismiss_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testDismiss_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromHome() {
+        runTest(TestSurface.HOME, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromHome() {
+        runTest(TestSurface.HOME, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromHome() {
+        runTest(TestSurface.HOME, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    private void runTest(@NonNull TestSurface testSurface, @NonNull TestCase testCase) {
+        for (int i = 0; i < testCase.mNumAdditionalRunningTasks; i++) {
+            startTestActivity(3 + i);
+        }
+
+        KeyboardQuickSwitch kqs;
+        switch (testSurface) {
+            case HOME:
+                kqs = mLauncher.goHome().showQuickSwitchView();
+                break;
+            case LAUNCHED_APP:
+                mLauncher.setIgnoreTaskbarVisibility(true);
+                kqs = mLauncher.getLaunchedAppState().showQuickSwitchView();
+                break;
+            case HOME_ALL_APPS:
+                kqs = mLauncher.goHome().switchToAllApps().showQuickSwitchView();
+                break;
+            case WIDGETS:
+                kqs = mLauncher.goHome().openAllWidgets().showQuickSwitchView();
+                break;
+            default:
+                throw new IllegalStateException(
+                        "KeyboardQuickSwitch could not be initialized for test surface: "
+                            + testSurface);
+        }
+
+        switch (testCase) {
+            case DISMISS:
+                kqs.dismiss();
+                break;
+            case LAUNCH_LAST_APP:
+                kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
+                break;
+            case LAUNCH_SELECTED_APP:
+                kqs.moveFocusForward().launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
+                break;
+            case LAUNCH_OVERVIEW:
+                kqs.moveFocusBackward().moveFocusBackward().launchFocusedOverviewTask();
+                break;
+            default:
+                throw new IllegalStateException("Cannot run test case: " + testCase);
+        }
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 5531c6e..3039261 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -17,10 +17,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
-import static com.android.launcher3.testing.shared.TestProtocol.FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP;
-import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ICON_MENU;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
@@ -32,7 +29,6 @@
 
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
-import android.util.Log;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -51,11 +47,9 @@
 import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 import com.android.quickstep.views.RecentsView;
@@ -89,6 +83,7 @@
     @After
     public void tearDown() {
         executeOnLauncher(launcher -> {
+            if (launcher == null) return;
             RecentsView recentsView = launcher.getOverviewPanel();
             recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false);
         });
@@ -203,6 +198,7 @@
     }
 
 
+    @PlatinumTest(focusArea = "launcher")
     @Test
     public void testOverviewActionsMenu() throws Exception {
         startTestAppsWithCheck();
@@ -215,6 +211,23 @@
                 isInLaunchedApp(launcher)));
     }
 
+
+    @Test
+    public void testOverviewActionsMenu_iconAppChipMenu() throws Exception {
+        try (AutoCloseable c = TestUtil.overrideFlag(ENABLE_OVERVIEW_ICON_MENU, true)) {
+            startTestAppsWithCheck();
+
+            OverviewTaskMenu menu =
+                    mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+
+            assertNotNull("Tapping App info menu item returned null", menu.tapAppInfoMenuItem());
+            executeOnLauncher(launcher -> assertTrue(
+                    "Launcher activity is the top activity; expecting another activity to be the "
+                            + "top",
+                    isInLaunchedApp(launcher)));
+        }
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
@@ -320,7 +333,7 @@
     @Test
     @ScreenRecord // b/242163205
     @PlatinumTest(focusArea = "launcher")
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/286084688
+    @TaskbarModeSwitch(mode = PERSISTENT)
     public void testQuickSwitchToPreviousAppForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
         startTestActivity(2);
@@ -340,16 +353,7 @@
                 "The first app we should have quick switched to is not running");
         // Expect task bar visible when the launched app was the test activity.
         launchedAppState = getAndAssertLaunchedApp();
-
-        Log.e(FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP,
-                "is Taskbar Transient : " + DisplayController.isTransientTaskbar(mTargetContext));
-        // TODO(b/286084688): Remove this branching check after test corruption is resolved.
-        // Branching this check because of test corruption.
-        if (DisplayController.isTransientTaskbar(mTargetContext)) {
-            launchedAppState.assertTaskbarHidden();
-        } else {
-            launchedAppState.assertTaskbarVisible();
-        }
+        launchedAppState.assertTaskbarVisible();
     }
 
     @Test
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index cc56faf..ed152f2 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ICON_MENU;
 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
 import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
@@ -30,8 +31,10 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
@@ -139,6 +142,42 @@
                         .hasMenuItem("Save app pair"));
     }
 
+    @Test
+    public void testTapBothIconMenus() {
+        createAndLaunchASplitPair();
+
+        OverviewTaskMenu taskMenu =
+                mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+        assertTrue("App info item not appearing in expanded task menu.",
+                taskMenu.hasMenuItem("App info"));
+        taskMenu.touchOutsideTaskMenuToDismiss();
+
+        OverviewTaskMenu splitMenu =
+                mLauncher.getOverview().getCurrentTask().tapSplitTaskMenu();
+        assertTrue("App info item not appearing in expanded split task's menu.",
+                splitMenu.hasMenuItem("App info"));
+        splitMenu.touchOutsideTaskMenuToDismiss();
+    }
+
+    @Test
+    public void testTapBothIconMenus_iconAppChipMenu() throws Exception {
+        try (AutoCloseable c = TestUtil.overrideFlag(ENABLE_OVERVIEW_ICON_MENU, true)) {
+            createAndLaunchASplitPair();
+
+            OverviewTaskMenu taskMenu =
+                    mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+            assertTrue("App info item not appearing in expanded task menu.",
+                    taskMenu.hasMenuItem("App info"));
+            taskMenu.touchOutsideTaskMenuToDismiss();
+
+            OverviewTaskMenu splitMenu =
+                    mLauncher.getOverview().getCurrentTask().tapSplitTaskMenu();
+            assertTrue("App info item not appearing in expanded split task's menu.",
+                    splitMenu.hasMenuItem("App info"));
+            splitMenu.touchOutsideTaskMenuToDismiss();
+        }
+    }
+
     private void createAndLaunchASplitPair() {
         startTestActivity(2);
         startTestActivity(3);
diff --git a/res/drawable/icon_menu_arrow_background.xml b/res/drawable/icon_menu_arrow_background.xml
new file mode 100644
index 0000000..f24022e
--- /dev/null
+++ b/res/drawable/icon_menu_arrow_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:autoMirrored="true">
+    <gradient
+        android:type="linear"
+        android:angle="0"
+        android:startColor="#00000000"
+        android:centerX="0.25"
+        android:centerColor="?androidprv:attr/materialColorSurfaceContainer"
+        android:endColor="?androidprv:attr/materialColorSurfaceContainer" />
+    <corners android:radius="@dimen/dialogCornerRadius" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/icon_menu_background.xml b/res/drawable/icon_menu_background.xml
new file mode 100644
index 0000000..ec5f011
--- /dev/null
+++ b/res/drawable/icon_menu_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+    <solid android:color="?androidprv:attr/materialColorSurfaceContainer" />
+    <corners android:radius="@dimen/dialogCornerRadius" />
+</shape>
\ No newline at end of file
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index b5e4012..aa96397 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -60,7 +60,7 @@
     <string name="all_apps_loading_message" msgid="5813968043155271636">"Laai tans programme …"</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"Kon geen programme kry wat by \"<xliff:g id="QUERY">%1$s</xliff:g>\" pas nie"</string>
     <string name="label_application" msgid="8531721983832654978">"Program"</string>
-    <string name="all_apps_label" msgid="5015784846527570951">"Alle programme"</string>
+    <string name="all_apps_label" msgid="5015784846527570951">"Alle apps"</string>
     <string name="notifications_header" msgid="1404149926117359025">"Kennisgewings"</string>
     <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Raak en hou om \'n kortpad te skuif."</string>
     <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"Dubbeltik en hou om \'n kortpad te skuif of gebruik gepasmaakte handelinge."</string>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 10f47cb..7661bd7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -399,6 +399,10 @@
     <dimen name="task_thumbnail_icon_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size_grid">0dp</dimen>
+    <dimen name="task_thumbnail_icon_menu_max_width">0dp</dimen>
+    <dimen name="task_thumbnail_icon_menu_drawable_size">0dp</dimen>
+    <dimen name="task_thumbnail_icon_menu_drawable_touch_size">0dp</dimen>
+    <dimen name="task_menu_vertical_padding">0dp</dimen>
     <dimen name="overview_task_margin">0dp</dimen>
     <dimen name="overview_actions_height">0dp</dimen>
     <dimen name="overview_actions_button_spacing">0dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ab9836f..879000a 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -17,7 +17,6 @@
 package com.android.launcher3;
 
 import static android.text.Layout.Alignment.ALIGN_NORMAL;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
 import static com.android.launcher3.config.FeatureFlags.enableCursorHoverStates;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
@@ -878,7 +877,7 @@
             if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0
                     || info.hasPromiseIconUi()
                     || (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0
-                    || (ENABLE_DOWNLOAD_APP_UX_V2.get() && icon != null)) {
+                    || (icon != null)) {
                 updateProgressBarUi(info.getProgressLevel() == 100 ? icon : null);
             }
         }
@@ -915,9 +914,7 @@
             if (mIcon instanceof PreloadIconDrawable) {
                 preloadIconDrawable = (PreloadIconDrawable) mIcon;
                 preloadIconDrawable.setLevel(progressLevel);
-                preloadIconDrawable.setIsDisabled(ENABLE_DOWNLOAD_APP_UX_V2.get()
-                        ? info.getProgressLevel() == 0
-                        : !info.isAppStartable());
+                preloadIconDrawable.setIsDisabled(info.getProgressLevel() == 0);
             } else {
                 preloadIconDrawable = makePreloadIcon();
                 setIcon(preloadIconDrawable);
@@ -942,9 +939,7 @@
         final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info);
 
         preloadDrawable.setLevel(progressLevel);
-        preloadDrawable.setIsDisabled(ENABLE_DOWNLOAD_APP_UX_V2.get()
-                ? info.getProgressLevel() == 0
-                : !info.isAppStartable());
+        preloadDrawable.setIsDisabled(info.getProgressLevel() == 0);
         return preloadDrawable;
     }
 
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index d8804a1..9a2193f 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.Utilities.pxFromSp;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH;
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.icons.GraphicsUtils.getShapePath;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
@@ -250,6 +251,7 @@
     public int overviewTaskIconSizePx;
     public int overviewTaskIconDrawableSizePx;
     public int overviewTaskIconDrawableSizeGridPx;
+    public int overviewTaskIconAppChipMenuDrawableSizePx;
     public int overviewTaskThumbnailTopMarginPx;
     public final int overviewActionsHeight;
     public final int overviewActionsTopMarginPx;
@@ -614,12 +616,17 @@
         desiredWorkspaceHorizontalMarginOriginalPx = desiredWorkspaceHorizontalMarginPx;
 
         overviewTaskMarginPx = res.getDimensionPixelSize(R.dimen.overview_task_margin);
-        overviewTaskIconSizePx = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_size);
+        overviewTaskIconSizePx = enableOverviewIconMenu() ? res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_drawable_touch_size) : res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_size);
         overviewTaskIconDrawableSizePx =
                 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size);
         overviewTaskIconDrawableSizeGridPx =
                 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size_grid);
-        overviewTaskThumbnailTopMarginPx = overviewTaskIconSizePx + overviewTaskMarginPx;
+        overviewTaskIconAppChipMenuDrawableSizePx = res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_drawable_size);
+        overviewTaskThumbnailTopMarginPx =
+                enableOverviewIconMenu() ? 0 : overviewTaskIconSizePx + overviewTaskMarginPx;
         // Don't add margin with floating search bar to minimize risk of overlapping.
         overviewActionsTopMarginPx = FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() ? 0
                 : res.getDimensionPixelSize(R.dimen.overview_actions_top_margin);
@@ -698,6 +705,17 @@
     }
 
     /**
+     * Return maximum of all apps row count displayed on screen. Note that 1) Partially displayed
+     * row is counted as 1 row, and 2) we don't exclude the space of floating search bar. This
+     * method is used for calculating number of {@link BubbleTextView} we need to pre-inflate. Thus
+     * reasonable over estimation is fine.
+     */
+    public int getMaxAllAppsRowCount() {
+        return (int) (Math.ceil((availableHeightPx - allAppsTopPadding)
+                / (float) allAppsCellHeightPx));
+    }
+
+    /**
      * QSB width is always calculated because when in 3 button nav the width doesn't follow the
      * width of the hotseat.
      */
@@ -2016,6 +2034,8 @@
                 overviewTaskIconDrawableSizePx));
         writer.println(prefix + pxToDpStr("overviewTaskIconDrawableSizeGridPx",
                 overviewTaskIconDrawableSizeGridPx));
+        writer.println(prefix + pxToDpStr("overviewTaskIconAppChipMenuDrawableSizePx",
+                overviewTaskIconAppChipMenuDrawableSizePx));
         writer.println(prefix + pxToDpStr("overviewTaskThumbnailTopMarginPx",
                 overviewTaskThumbnailTopMarginPx));
         writer.println(prefix + pxToDpStr("overviewActionsTopMarginPx",
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 29fd0eb..66a35d7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -3027,6 +3027,7 @@
         mStateManager.dump(prefix, writer);
         mPopupDataProvider.dump(prefix, writer);
         mDeviceProfile.dump(this, prefix, writer);
+        mAppsView.getAppsStore().dump(prefix, writer);
 
         try {
             FileLog.flushAll(writer);
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 7edbeac..cffddfc 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN;
@@ -96,16 +97,17 @@
     protected void updatePoolSize() {
         DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile();
         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
-        int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
 
-        // If all apps' hidden visibility is INVISIBLE, we will need to preinflate one page of
-        // all apps icons for smooth scrolling.
-        int maxPoolSizeForAppIcons = (approxRows + 1) * grid.numShownAllAppsColumns;
-        if (ALL_APPS_GONE_VISIBILITY.get()) {
-            // If all apps' hidden visibility is GONE, we need to increase prefinated icons number
-            // by [PREINFLATE_ICONS_ROW_COUNT] rows + [EXTRA_ICONS_COUNT] for fast opening all apps.
+        // By default the max num of pool size for app icons is num of app icons in one page of
+        // all apps.
+        int maxPoolSizeForAppIcons = grid.getMaxAllAppsRowCount()
+                * grid.numShownAllAppsColumns;
+        if (ALL_APPS_GONE_VISIBILITY.get() && ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+            // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to
+            // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows +
+            // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly.
             maxPoolSizeForAppIcons +=
                     PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT;
         }
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 378dbf3..7867f44 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.views.ActivityContext;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -238,4 +239,13 @@
     public interface OnUpdateListener {
         void onAppsUpdated();
     }
+
+    /** Generate a dumpsys for each app package name and position in the apps list */
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "\tAllAppsStore Apps[] size: " + mApps.length);
+        for (int i = 0; i < mApps.length; i++) {
+            writer.println(String.format("%s\tPackage index and name: %d/%s", prefix, i,
+                    mApps[i].componentName.getPackageName()));
+        }
+    }
 }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 6902ea7..ddbec64 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -152,21 +152,9 @@
     // TODO(Block 8): Clean up flags
 
     // TODO(Block 9): Clean up flags
-    public static final BooleanFlag ENABLE_DOWNLOAD_APP_UX_V2 = getReleaseFlag(270395134,
-            "ENABLE_DOWNLOAD_APP_UX_V2", ENABLED, "Updates the download app UX"
-                    + " to have better visuals");
-
-    public static final BooleanFlag ENABLE_DOWNLOAD_APP_UX_V3 = getDebugFlag(270395186,
-            "ENABLE_DOWNLOAD_APP_UX_V3", ENABLED, "Updates the download app UX"
-                    + " to have better visuals, improve contrast, and color");
 
     public static final BooleanFlag SHOW_DOT_PAGINATION = getDebugFlag(270395278,
             "SHOW_DOT_PAGINATION", ENABLED, "Enable showing dot pagination in workspace");
-
-    public static final BooleanFlag LARGE_SCREEN_WIDGET_PICKER = getDebugFlag(270395809,
-            "LARGE_SCREEN_WIDGET_PICKER", ENABLED, "Enable new widget picker that takes "
-                    + "advantage of large screen format");
-
     public static final BooleanFlag UNFOLDED_WIDGET_PICKER = getDebugFlag(301918659,
             "UNFOLDED_WIDGET_PICKER", DISABLED, "Enable new widget picker that takes "
                     + "advantage of the unfolded foldable format");
@@ -332,6 +320,15 @@
         return ENABLE_GRID_ONLY_OVERVIEW.get() || Flags.enableGridOnlyOverview();
     }
 
+    // Aconfig migration complete for ENABLE_OVERVIEW_ICON_MENU.
+    @VisibleForTesting
+    public static final BooleanFlag ENABLE_OVERVIEW_ICON_MENU = getDebugFlag(257950105,
+            "ENABLE_OVERVIEW_ICON_MENU", TEAMFOOD,
+            "Enable updated overview icon and menu within task.");
+    public static boolean enableOverviewIconMenu() {
+        return ENABLE_OVERVIEW_ICON_MENU.get() || Flags.enableOverviewIconMenu();
+    }
+
     // Aconfig migration complete for ENABLE_CURSOR_HOVER_STATES.
     @VisibleForTesting
     public static final BooleanFlag ENABLE_CURSOR_HOVER_STATES = getDebugFlag(243191650,
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 307052a..3e77c78 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -19,8 +19,6 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V3;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -76,10 +74,8 @@
     // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
     private static final float COMPLETE_ANIM_FRACTION = 1f;
 
-    private static final float SMALL_SCALE = ENABLE_DOWNLOAD_APP_UX_V3.get() ? 0.8f : 0.7f;
-    private static final float PROGRESS_STROKE_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get()
-            ? 0.055f
-            : 0.075f;
+    private static final float SMALL_SCALE = 0.8f;
+    private static final float PROGRESS_STROKE_SCALE = 0.055f;
     private static final float PROGRESS_BOUNDS_SCALE = 0.075f;
     private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
     private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
@@ -119,8 +115,6 @@
 
     private ObjectAnimator mCurrentAnim;
 
-    private boolean mIsStartable;
-
     public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
         this(
                 info,
@@ -144,9 +138,7 @@
 
         mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
         mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
-        if (ENABLE_DOWNLOAD_APP_UX_V3.get()) {
-            mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
-        }
+        mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
         mIndicatorColor = indicatorColor;
 
         // This is the color
@@ -181,9 +173,6 @@
         mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);
 
         setLevel(info.getProgressLevel());
-        if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
-            setIsStartable(info.isAppStartable());
-        }
     }
 
     @Override
@@ -212,54 +201,31 @@
             return;
         }
 
-        if (mInternalStateProgress > 0
-                && (ENABLE_DOWNLOAD_APP_UX_V3.get() || !ENABLE_DOWNLOAD_APP_UX_V2.get())) {
+        if (mInternalStateProgress > 0) {
             // Draw background.
-            mProgressPaint.setStyle(ENABLE_DOWNLOAD_APP_UX_V3.get()
-                    ? Paint.Style.FILL
-                    : Paint.Style.FILL_AND_STROKE);
-            mProgressPaint.setColor(ENABLE_DOWNLOAD_APP_UX_V3.get()
-                    ? mPlateColor
-                    : mSystemBackgroundColor);
+            mProgressPaint.setStyle(Paint.Style.FILL);
+            mProgressPaint.setColor(mPlateColor);
             canvas.drawPath(mScaledTrackPath, mProgressPaint);
         }
 
-        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
+        if (mInternalStateProgress > 0) {
             // Draw track and progress.
             mProgressPaint.setStyle(Paint.Style.STROKE);
-            mProgressPaint.setColor(ENABLE_DOWNLOAD_APP_UX_V3.get()
-                    ? mTrackColor
-                    : mSystemAccentColor);
-            if (!ENABLE_DOWNLOAD_APP_UX_V3.get()) {
-                mProgressPaint.setAlpha(TRACK_ALPHA);
-            }
+            mProgressPaint.setColor(mTrackColor);
             canvas.drawPath(mScaledTrackPath, mProgressPaint);
             mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
-            if (ENABLE_DOWNLOAD_APP_UX_V3.get()) {
-                mProgressPaint.setColor(mProgressColor);
-            }
+            mProgressPaint.setColor(mProgressColor);
             canvas.drawPath(mScaledProgressPath, mProgressPaint);
         }
 
         int saveCount = canvas.save();
-        float scale = ENABLE_DOWNLOAD_APP_UX_V2.get()
-                ? 1 - mIconScaleMultiplier.value * (1 - SMALL_SCALE)
-                : SMALL_SCALE;
+        float scale = 1 - mIconScaleMultiplier.value * (1 - SMALL_SCALE);
         canvas.scale(scale, scale, bounds.exactCenterX(), bounds.exactCenterY());
 
         super.drawInternal(canvas, bounds);
         canvas.restoreToCount(saveCount);
     }
 
-    @Override
-    protected void updateFilter() {
-        if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
-            setAlpha(mIsDisabled ? DISABLED_ICON_ALPHA : MAX_PAINT_ALPHA);
-        } else {
-            super.updateFilter();
-        }
-    }
-
     /**
      * Updates the install progress based on the level
      */
@@ -296,14 +262,6 @@
         return !mRanFinishAnimation;
     }
 
-    /** Sets whether this icon should display the startable app UI. */
-    public void setIsStartable(boolean isStartable) {
-        if (mIsStartable != isStartable) {
-            mIsStartable = isStartable;
-            setIsDisabled(!isStartable);
-        }
-    }
-
     private void updateInternalState(
             float finalProgress, boolean isFinish, Runnable onFinishCallback) {
         if (mCurrentAnim != null) {
@@ -355,7 +313,7 @@
      */
     private void setInternalProgress(float progress) {
         // Animate scale and alpha from pending to downloading state.
-        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && progress > 0 && mInternalStateProgress == 0) {
+        if (progress > 0 && mInternalStateProgress == 0) {
             // Progress is changing for the first time, animate the icon scale
             Animator iconScaleAnimator = mIconScaleMultiplier.animateToValue(1);
             iconScaleAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
@@ -365,14 +323,11 @@
 
         mInternalStateProgress = progress;
         if (progress <= 0) {
-            if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
-                mScaledTrackPath.reset();
-            }
             mIconScaleMultiplier.updateValue(0);
         } else {
             mPathMeasure.getSegment(
                     0, Math.min(progress, 1) * mTrackLength, mScaledProgressPath, true);
-            if (progress > 1 && ENABLE_DOWNLOAD_APP_UX_V2.get()) {
+            if (progress > 1) {
                 // map the scale back to original value
                 mIconScaleMultiplier.updateValue(Utilities.mapBoundToRange(
                         progress - 1, 0, COMPLETE_ANIM_FRACTION, 1, 0, EMPHASIZED));
diff --git a/src/com/android/launcher3/logging/StartupLatencyLogger.kt b/src/com/android/launcher3/logging/StartupLatencyLogger.kt
index 93e9de5..7d7564b 100644
--- a/src/com/android/launcher3/logging/StartupLatencyLogger.kt
+++ b/src/com/android/launcher3/logging/StartupLatencyLogger.kt
@@ -30,6 +30,11 @@
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     var workspaceLoadStartTime: Long = UNSET_LONG
 
+    // StartupLatencyLogger should only send launcher startup logs once in each launcher activity
+    // lifecycle. After launcher activity startup is completed, the logger should be torn down and
+    // reject all logging calls. This flag should be checked at all APIs to prevent logging invalid
+    // startup metrics (such as loading workspace in screen rotation).
+    var isTornDown = false
     private var isInTest = false
 
     /** Subclass can override this method to handle collected latency metrics. */
@@ -45,6 +50,9 @@
     @MainThread
     fun logWorkspaceLoadStartTime(startTimeMs: Long): StartupLatencyLogger {
         Preconditions.assertUIThread()
+        if (isTornDown) {
+            return this
+        }
         workspaceLoadStartTime = startTimeMs
         return this
     }
@@ -56,6 +64,9 @@
     @MainThread
     fun logCardinality(cardinality: Int): StartupLatencyLogger {
         Preconditions.assertUIThread()
+        if (isTornDown) {
+            return this
+        }
         this.cardinality = cardinality
         return this
     }
@@ -67,6 +78,9 @@
     fun logStart(event: LauncherLatencyEvent, startTimeMs: Long): StartupLatencyLogger {
         // In unit test no looper is attached to current thread
         Preconditions.assertUIThread()
+        if (isTornDown) {
+            return this
+        }
         if (validateLoggingEventAtStart(event)) {
             startTimeByEvent.put(event.id, startTimeMs)
         }
@@ -80,6 +94,9 @@
     fun logEnd(event: LauncherLatencyEvent, endTimeMs: Long): StartupLatencyLogger {
         // In unit test no looper is attached to current thread
         Preconditions.assertUIThread()
+        if (isTornDown) {
+            return this
+        }
         maybeLogStartOfWorkspaceLoadTime(event)
         if (validateLoggingEventAtEnd(event)) {
             endTimeByEvent.put(event.id, endTimeMs)
@@ -96,6 +113,7 @@
         endTimeByEvent.clear()
         cardinality = UNSET_INT
         workspaceLoadStartTime = UNSET_LONG
+        isTornDown = true
     }
 
     @MainThread
@@ -181,6 +199,15 @@
                 "Cannot end ${event.name} event after ${LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.name}",
             )
             return false
+        } else if (
+            latencyType == LatencyType.COLD_DEVICE_REBOOTING &&
+                event == LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC
+        ) {
+            Log.e(
+                TAG,
+                "Cannot have ${LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC.name} in ${LatencyType.COLD_DEVICE_REBOOTING.name} startup type"
+            )
+            return false
         }
         return true
     }
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 1917ba2..37a7171 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -44,7 +44,6 @@
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -68,7 +67,8 @@
  */
 public class PackageUpdatedTask extends BaseModelUpdateTask {
 
-    private static boolean DEBUG = false;
+    // TODO(b/290090023): Set to false after root causing is done.
+    private static final boolean DEBUG = true;
     private static final String TAG = "PackageUpdatedTask";
 
     public static final int OP_NONE = 0;
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 3c59c1d..1d71805 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -93,9 +93,7 @@
                 EXTRA_ICONS_COUNT
         if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) {
             val grid = ActivityContext.lookupContext<T>(context).deviceProfile
-            val approxRows =
-                Math.ceil((grid.availableHeightPx / grid.allAppsIconSizePx).toDouble()).toInt()
-            targetPreinflateCount += (approxRows + 1) * grid.numShownAllAppsColumns
+            targetPreinflateCount += grid.maxAllAppsRowCount * grid.numShownAllAppsColumns
         }
         val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON)
         return targetPreinflateCount - existingPreinflateCount
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index c356da9..d434ad2 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -27,6 +27,7 @@
 
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
@@ -267,13 +268,19 @@
 
     @Override
     public float getTaskMenuX(float x, View thumbnailView,
-            DeviceProfile deviceProfile, float taskInsetMargin) {
+            DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
+        if (enableOverviewIconMenu()) {
+            return x - (taskInsetMargin / 2f);
+        }
         return thumbnailView.getMeasuredWidth() + x - taskInsetMargin;
     }
 
     @Override
     public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin) {
+            View taskMenuView, float taskInsetMargin, View taskViewIcon) {
+        if (enableOverviewIconMenu()) {
+            return y - taskMenuView.getMeasuredHeight() - taskInsetMargin;
+        }
         BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
         int taskMenuWidth = lp.width;
         if (stagePosition == STAGE_POSITION_UNDEFINED) {
@@ -537,14 +544,25 @@
     }
 
     @Override
+    public void setTaskIconMenuParams(FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin,
+            int thumbnailTopMargin) {
+        iconMenuParams.gravity = END | TOP;
+        iconMenuParams.setMarginStart(0);
+        iconMenuParams.topMargin = iconMenuParams.width + iconMenuMargin;
+        iconMenuParams.bottomMargin = 0;
+        iconMenuParams.setMarginEnd(iconMenuMargin);
+    }
+
+    @Override
     public void setSplitIconParams(View primaryIconView, View secondaryIconView,
             int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
             int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
             DeviceProfile deviceProfile, SplitBounds splitConfig) {
         FrameLayout.LayoutParams primaryIconParams =
                 (FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
-        FrameLayout.LayoutParams secondaryIconParams =
-                new FrameLayout.LayoutParams(primaryIconParams);
+        FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu()
+                ? (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams()
+                : new FrameLayout.LayoutParams(primaryIconParams);
 
         // We calculate the "midpoint" of the thumbnail area, and place the icons there.
         // This is the place where the thumbnail area splits by default, in a near-50/50 split.
@@ -560,11 +578,17 @@
         int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct);
         int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct);
 
-        primaryIconParams.gravity = BOTTOM | (isRtl ? START : END);
-        secondaryIconParams.gravity = BOTTOM | (isRtl ? START : END);
+        primaryIconParams.gravity = enableOverviewIconMenu() ? TOP | (isRtl ? START : END)
+                : BOTTOM | (isRtl ? START : END);
+        secondaryIconParams.gravity = enableOverviewIconMenu() ? TOP | (isRtl ? START : END)
+                : BOTTOM | (isRtl ? START : END);
         primaryIconView.setTranslationX(0);
         secondaryIconView.setTranslationX(0);
-        if (splitConfig.initiatedFromSeascape) {
+        if (enableOverviewIconMenu()) {
+            int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
+                    splitConfig.visualDividerBounds.height());
+            secondaryIconView.setTranslationY(primarySnapshotHeight + dividerThickness);
+        } else if (splitConfig.initiatedFromSeascape) {
             // if the split was initiated from seascape,
             // the task on the right (secondary) is slightly larger
             primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset);
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 39ef129..0069d9b 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -173,6 +173,8 @@
     // Overview TaskMenuView methods
     void setTaskIconParams(FrameLayout.LayoutParams iconParams,
             int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl);
+    void setTaskIconMenuParams(FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin,
+            int thumbnailTopMargin);
     void setSplitIconParams(View primaryIconView, View secondaryIconView,
             int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
             int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
@@ -185,9 +187,9 @@
      * getTaskMenuWidth()), so we directly use that in the calculations.
      */
     float getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile,
-            float taskInsetMargin);
+            float taskInsetMargin, View taskViewIcon);
     float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin);
+            View taskMenuView, float taskInsetMargin, View taskViewIcon);
     int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
             @StagePosition int stagePosition);
     /**
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index dc4621e..b3189b7 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -21,11 +21,13 @@
 import static android.view.Gravity.END;
 import static android.view.Gravity.START;
 import static android.view.Gravity.TOP;
+import static android.view.View.LAYOUT_DIRECTION_RTL;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
@@ -264,8 +266,11 @@
 
     @Override
     public float getTaskMenuX(float x, View thumbnailView,
-            DeviceProfile deviceProfile, float taskInsetMargin) {
-        if (deviceProfile.isLandscape) {
+            DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
+        if (enableOverviewIconMenu()) {
+            return x + (thumbnailView.getLayoutDirection() == LAYOUT_DIRECTION_RTL
+                    ? -(taskViewIcon.getWidth() / 2f) : 0);
+        } else if (deviceProfile.isLandscape) {
             return x + taskInsetMargin
                     + (thumbnailView.getMeasuredWidth() - thumbnailView.getMeasuredHeight()) / 2f;
         } else {
@@ -275,13 +280,22 @@
 
     @Override
     public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin) {
+            View taskMenuView, float taskInsetMargin, View taskViewIcon) {
+        if (enableOverviewIconMenu()) {
+            return y;
+        }
         return y + taskInsetMargin;
     }
 
     @Override
     public int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
             @StagePosition int stagePosition) {
+        if (enableOverviewIconMenu()) {
+            int padding = thumbnailView.getResources().getDimensionPixelSize(
+                    R.dimen.task_menu_vertical_padding);
+            return thumbnailView.getResources().getDimensionPixelSize(
+                    R.dimen.task_thumbnail_icon_menu_max_width) + (2 * padding);
+        }
         return deviceProfile.isLandscape && !deviceProfile.isTablet
                 ? thumbnailView.getMeasuredHeight()
                 : thumbnailView.getMeasuredWidth();
@@ -689,16 +703,53 @@
     }
 
     @Override
+    public void setTaskIconMenuParams(FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin,
+            int thumbnailTopMargin) {
+        iconMenuParams.gravity = TOP | START;
+        iconMenuParams.setMarginStart(iconMenuMargin);
+        iconMenuParams.topMargin = iconMenuMargin;
+        iconMenuParams.bottomMargin = 0;
+        iconMenuParams.setMarginEnd(0);
+    }
+
+    @Override
     public void setSplitIconParams(View primaryIconView, View secondaryIconView,
             int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
             int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
             DeviceProfile deviceProfile, SplitBounds splitConfig) {
         FrameLayout.LayoutParams primaryIconParams =
                 (FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
-        FrameLayout.LayoutParams secondaryIconParams =
-                new FrameLayout.LayoutParams(primaryIconParams);
+        FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu()
+                ? (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams()
+                : new FrameLayout.LayoutParams(primaryIconParams);
 
-        if (deviceProfile.isLandscape) {
+        if (enableOverviewIconMenu()) {
+            primaryIconParams.gravity = TOP | START;
+            secondaryIconParams.gravity = TOP | START;
+            secondaryIconParams.topMargin = primaryIconParams.topMargin;
+            secondaryIconParams.setMarginStart(primaryIconParams.getMarginStart());
+            if (deviceProfile.isLandscape) {
+                int fullscreenInsetThickness = deviceProfile.isSeascape()
+                        ? deviceProfile.getInsets().right
+                        : deviceProfile.getInsets().left;
+                int fullscreenMidpointFromBottom = ((deviceProfile.widthPx
+                        - fullscreenInsetThickness) / 2);
+                float midpointFromEndPct = (float) fullscreenMidpointFromBottom
+                        / deviceProfile.widthPx;
+                int bottomToMidpointOffset = (int) (groupedTaskViewWidth * midpointFromEndPct);
+                if (isRtl) {
+                    primaryIconView.setTranslationX(-bottomToMidpointOffset);
+                } else {
+                    secondaryIconView.setTranslationX(bottomToMidpointOffset);
+                }
+            } else {
+                secondaryIconView.setTranslationX(0);
+                int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
+                        splitConfig.visualDividerBounds.height());
+                secondaryIconView.setTranslationY(
+                        primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness));
+            }
+        } else if (deviceProfile.isLandscape) {
             // We calculate the "midpoint" of the thumbnail area, and place the icons there.
             // This is the place where the thumbnail area splits by default, in a near-50/50 split.
             // It is usually not exactly 50/50, due to insets/screen cutouts.
@@ -754,8 +805,10 @@
             secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
             secondaryIconView.setTranslationX(taskIconHeight / 2f);
         }
-        primaryIconView.setTranslationY(0);
-        secondaryIconView.setTranslationY(0);
+        if (!enableOverviewIconMenu()) {
+            primaryIconView.setTranslationY(0);
+            secondaryIconView.setTranslationY(0);
+        }
 
         primaryIconView.setLayoutParams(primaryIconParams);
         secondaryIconView.setLayoutParams(secondaryIconParams);
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index ec01231..dac7964 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -22,6 +22,7 @@
 import static android.view.Gravity.RIGHT;
 import static android.view.Gravity.START;
 
+import static com.android.launcher3.config.FeatureFlags.enableOverviewIconMenu;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
@@ -86,13 +87,19 @@
 
     @Override
     public float getTaskMenuX(float x, View thumbnailView,
-            DeviceProfile deviceProfile, float taskInsetMargin) {
+            DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
+        if (enableOverviewIconMenu()) {
+            return x + taskViewIcon.getHeight() + taskInsetMargin * 2;
+        }
         return x + taskInsetMargin;
     }
 
     @Override
     public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin) {
+            View taskMenuView, float taskInsetMargin, View taskViewIcon) {
+        if (enableOverviewIconMenu()) {
+            return y + taskViewIcon.getWidth() - taskViewIcon.getHeight();
+        }
         BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
         int taskMenuWidth = lp.width;
         if (stagePosition == STAGE_POSITION_UNDEFINED) {
@@ -208,13 +215,24 @@
     public void setTaskIconParams(FrameLayout.LayoutParams iconParams,
             int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
         iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
-        iconParams.leftMargin = -taskIconHeight - taskIconMargin / 2;
+        iconParams.leftMargin =
+                enableOverviewIconMenu() ? 0 : -taskIconHeight - taskIconMargin / 2;
         iconParams.rightMargin = 0;
-        iconParams.topMargin = thumbnailTopMargin / 2;
+        iconParams.topMargin = enableOverviewIconMenu() ? 0 : thumbnailTopMargin / 2;
         iconParams.bottomMargin = 0;
     }
 
     @Override
+    public void setTaskIconMenuParams(FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin,
+            int thumbnailTopMargin) {
+        iconMenuParams.gravity = BOTTOM | START;
+        iconMenuParams.setMarginStart(0);
+        iconMenuParams.topMargin = 0;
+        iconMenuParams.bottomMargin = 0;
+        iconMenuParams.setMarginEnd(0);
+    }
+
+    @Override
     public void setSplitIconParams(View primaryIconView, View secondaryIconView,
             int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
             int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
@@ -245,7 +263,11 @@
         secondaryIconParams.gravity = BOTTOM | (isRtl ? END : START);
         primaryIconView.setTranslationX(0);
         secondaryIconView.setTranslationX(0);
-        if (splitConfig.initiatedFromSeascape) {
+        if (enableOverviewIconMenu()) {
+            int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
+                    splitConfig.visualDividerBounds.height());
+            secondaryIconView.setTranslationY(-primarySnapshotHeight - dividerThickness);
+        } else if (splitConfig.initiatedFromSeascape) {
             // if the split was initiated from seascape,
             // the task on the right (secondary) is slightly larger
             primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index dcc86a1..742d2dc 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.widget;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
-import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER;
 
 import android.content.Context;
 import android.graphics.Canvas;
@@ -194,9 +193,7 @@
         int widthUsed;
         if (deviceProfile.isTablet) {
             int margin = deviceProfile.allAppsLeftRightMargin;
-            if (deviceProfile.isLandscape
-                    && LARGE_SCREEN_WIDGET_PICKER.get()
-                    && !deviceProfile.isTwoPanels) {
+            if (deviceProfile.isLandscape && !deviceProfile.isTwoPanels) {
                 margin = getResources().getDimensionPixelSize(
                         R.dimen.widget_picker_landscape_tablet_left_right_margin);
             }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 4105a9a..2c094f2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -18,7 +18,6 @@
 import static android.view.View.MeasureSpec.makeMeasureSpec;
 
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
@@ -679,8 +678,7 @@
 
     /** Shows the {@link WidgetsFullSheet} on the launcher. */
     public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
-        boolean isTwoPane = LARGE_SCREEN_WIDGET_PICKER.get()
-                && launcher.getDeviceProfile().isTablet
+        boolean isTwoPane = launcher.getDeviceProfile().isTablet
                 && launcher.getDeviceProfile().isLandscape
                 && (!launcher.getDeviceProfile().isTwoPanels
                     || FeatureFlags.UNFOLDED_WIDGET_PICKER.get());
@@ -798,8 +796,7 @@
         // Checks the orientation of the screen
         if (mOrientation != newConfig.orientation) {
             mOrientation = newConfig.orientation;
-            if (LARGE_SCREEN_WIDGET_PICKER.get()
-                    && mDeviceProfile.isTablet && !mDeviceProfile.isTwoPanels) {
+            if (mDeviceProfile.isTablet && !mDeviceProfile.isTwoPanels) {
                 handleClose(false);
                 show(Launcher.getLauncher(getContext()), false);
             } else {
diff --git a/tests/Android.bp b/tests/Android.bp
index e1b97de..62d232c 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -43,11 +43,13 @@
     name: "launcher-oop-tests-src",
     srcs: [
       "src/com/android/launcher3/allapps/OopTaplOpenCloseAllApps.java",
+      "src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java",
       "src/com/android/launcher3/dragging/TaplDragTest.java",
       "src/com/android/launcher3/dragging/TaplUninstallRemove.java",
       "src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
       "src/com/android/launcher3/ui/PortraitLandscapeRunner.java",
       "src/com/android/launcher3/ui/TaplTestsLauncher3.java",
+      "src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java",
       "src/com/android/launcher3/util/LauncherLayoutBuilder.java",
       "src/com/android/launcher3/util/TestUtil.java",
       "src/com/android/launcher3/util/Wait.java",
@@ -57,6 +59,7 @@
       "src/com/android/launcher3/util/rule/SamplerRule.java",
       "src/com/android/launcher3/util/rule/ScreenRecordRule.java",
       "src/com/android/launcher3/util/rule/ShellCommandRule.java",
+      "src/com/android/launcher3/util/rule/TestIsolationRule.java",
       "src/com/android/launcher3/util/rule/TestStabilityRule.java",
       "src/com/android/launcher3/util/rule/TISBindRule.java",
       "src/com/android/launcher3/util/viewcapture_analysis/*.java",
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
index b8f4c0b..ec32680 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
index a512277..d69be3f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
index a3a8dc5..7e92620 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
index 55066cb..a9bee2b 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
index 6e764c2..42b022b 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
index 7650082..53f8580 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
index 2b241a1..87189fa 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
index 6d38d27..0ade560 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
index 5799de7..d24457d 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
index b4956ff..38dc2c9 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
index 15afb61..5d23147 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
index 6cbed1f..5b53509 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
@@ -108,6 +108,7 @@
 	overviewTaskIconSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
+	overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
 	overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
 	overviewActionsTopMarginPx: 0.0px (0.0dp)
 	overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 0798e97..54a1c08 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -126,6 +126,7 @@
             "taskbar-all-apps-top-padding";
     public static final String REQUEST_ALL_APPS_TOP_PADDING = "all-apps-top-padding";
     public static final String REQUEST_ALL_APPS_BOTTOM_PADDING = "all-apps-bottom-padding";
+    public static final String REQUEST_REFRESH_OVERVIEW_TARGET = "refresh-overview-target";
 
     public static final String REQUEST_WORKSPACE_CELL_LAYOUT_SIZE = "workspace-cell-layout-size";
     public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center";
@@ -155,7 +156,6 @@
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
-    public static final String FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP = "b/286084688";
     public static final String ICON_MISSING = "b/282963545";
     public static final String INCORRECT_HOME_STATE = "b/293191790";
 
diff --git a/tests/src/com/android/launcher3/allapps/OopTaplOpenCloseAllApps.java b/tests/src/com/android/launcher3/allapps/OopTaplOpenCloseAllApps.java
index 7d6b7f9..f9dadaa 100644
--- a/tests/src/com/android/launcher3/allapps/OopTaplOpenCloseAllApps.java
+++ b/tests/src/com/android/launcher3/allapps/OopTaplOpenCloseAllApps.java
@@ -17,11 +17,14 @@
 
 import static com.android.launcher3.ui.TaplTestsLauncher3.expectFail;
 import static com.android.launcher3.ui.TaplTestsLauncher3.initialize;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import android.platform.test.annotations.PlatinumTest;
+
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
@@ -88,6 +91,7 @@
     /**
      * Make sure the swipe up gesture can take us back to the workspace from AllApps
      */
+    @PlatinumTest(focusArea = "launcher")
     @Test
     @PortraitLandscape
     public void testAllAppsSwipeUpToWorkspace() {
diff --git a/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java b/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java
new file mode 100644
index 0000000..85cf52e
--- /dev/null
+++ b/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.appiconmenu;
+
+import static com.android.launcher3.ui.TaplTestsLauncher3.APP_NAME;
+import static com.android.launcher3.ui.TaplTestsLauncher3.initialize;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.PlatinumTest;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.popup.ArrowPopup;
+import com.android.launcher3.tapl.AllApps;
+import com.android.launcher3.tapl.AppIconMenu;
+import com.android.launcher3.tapl.AppIconMenuItem;
+import com.android.launcher3.tapl.HomeAllApps;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * This test run in both Out of process (Oop) and in-process (Ipc).
+ * Tests the AppIconMenu (the menu that appears when you long press an app icon) and also make sure
+ * we can launch a shortcut from it.
+ */
+public class TaplAppIconMenuTest extends AbstractLauncherUiTest {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        initialize(this);
+    }
+
+    private boolean isOptionsPopupVisible(Launcher launcher) {
+        final ArrowPopup<?> popup = launcher.getOptionsPopup();
+        return popup != null && popup.isShown();
+    }
+
+    /**
+     * Open All apps then open the AppIconMenu then launch a shortcut from the menu and make sure it
+     * launches.
+     */
+    @Test
+    @PortraitLandscape
+    @PlatinumTest(focusArea = "launcher")
+    public void testLaunchMenuItem() {
+        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        allApps.freeze();
+        try {
+            final AppIconMenu menu = allApps.getAppIcon(APP_NAME).openDeepShortcutMenu();
+
+            executeOnLauncher(
+                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
+                            isOptionsPopupVisible(launcher)));
+
+            final AppIconMenuItem menuItem = menu.getMenuItem(1);
+            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
+            menuItem.launch(getAppPackageName());
+        } finally {
+            allApps.unfreeze();
+        }
+    }
+
+    /**
+     * Drag icon from AllApps to the workspace and then open the AppIconMenu and launch a shortcut
+     * from it.
+     */
+    @PlatinumTest(focusArea = "launcher")
+    @Test
+    public void testLaunchHomeScreenMenuItem() {
+        // Drag the test app icon to home screen and open short cut menu from the icon
+        final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        allApps.freeze();
+        try {
+            allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false);
+            final AppIconMenu menu = mLauncher.getWorkspace().getWorkspaceAppIcon(
+                    APP_NAME).openDeepShortcutMenu();
+
+            executeOnLauncher(
+                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
+                            isOptionsPopupVisible(launcher)));
+
+            final AppIconMenuItem menuItem = menu.getMenuItem(1);
+            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
+            menuItem.launch(getAppPackageName());
+        } finally {
+            allApps.unfreeze();
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/logging/StartupLatencyLoggerTest.kt b/tests/src/com/android/launcher3/logging/StartupLatencyLoggerTest.kt
index fffa6d7..a29218c 100644
--- a/tests/src/com/android/launcher3/logging/StartupLatencyLoggerTest.kt
+++ b/tests/src/com/android/launcher3/logging/StartupLatencyLoggerTest.kt
@@ -357,6 +357,7 @@
         assertThat(underTest.startTimeByEvent.size()).isEqualTo(4)
         assertThat(underTest.endTimeByEvent.size()).isEqualTo(4)
         assertThat(underTest.cardinality).isEqualTo(235)
+        assertThat(underTest.isTornDown).isFalse()
 
         underTest.reset()
 
@@ -364,5 +365,26 @@
         assertThat(underTest.endTimeByEvent.isEmpty()).isTrue()
         assertThat(underTest.cardinality).isEqualTo(StartupLatencyLogger.UNSET_INT)
         assertThat(underTest.workspaceLoadStartTime).isEqualTo(StartupLatencyLogger.UNSET_LONG)
+        assertThat(underTest.isTornDown).isTrue()
+    }
+
+    @Test
+    @UiThreadTest
+    fun tornDown_rejectLogs() {
+        underTest.reset()
+
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                100
+            )
+            .logEnd(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                200
+            )
+            .logCardinality(123)
+        assertThat(underTest.startTimeByEvent.isEmpty()).isTrue()
+        assertThat(underTest.endTimeByEvent.isEmpty()).isTrue()
+        assertThat(underTest.cardinality).isEqualTo(StartupLatencyLogger.UNSET_INT)
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 3e5d717..e837b8b 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -18,7 +18,6 @@
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
-import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import static org.junit.Assert.assertEquals;
@@ -68,6 +67,7 @@
 import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.util.rule.TestIsolationRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.rule.ViewCaptureRule;
 
@@ -111,15 +111,22 @@
     protected String mTargetPackage;
     private int mLauncherPid;
 
+    /** Detects activity leaks and throws an exception if a leak is found. */
     public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
+        checkDetectedLeaks(launcher, false);
+    }
+
+    /** Detects activity leaks and throws an exception if a leak is found. */
+    public static void checkDetectedLeaks(LauncherInstrumentation launcher,
+            boolean requireOneActiveActivity) {
         if (sActivityLeakReported) return;
 
         // Check whether activity leak detector has found leaked activities.
-        Wait.atMost(() -> getActivityLeakErrorMessage(launcher),
+        Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity),
                 () -> {
                     launcher.forceGc();
                     return MAIN_EXECUTOR.submit(
-                            () -> launcher.noLeakedActivities()).get();
+                            () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
                 }, DEFAULT_UI_TIMEOUT, launcher);
     }
 
@@ -127,13 +134,16 @@
         return getInstrumentation().getContext().getPackageName();
     }
 
-    private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher) {
+    private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher,
+            boolean requireOneActiveActivity) {
         sActivityLeakReported = true;
-        return "Activity leak detector has found leaked activities, "
-                + dumpHprofData(launcher, false) + ".";
+        return "Activity leak detector has found leaked activities, requirining 1 activity: "
+                + requireOneActiveActivity + "; "
+                + dumpHprofData(launcher, false, requireOneActiveActivity) + ".";
     }
 
-    public static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak) {
+    private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak,
+            boolean requireOneActiveActivity) {
         if (intentionalLeak) return "intentional leak; not generating dump";
 
         String result;
@@ -152,7 +162,7 @@
                             "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
                 }
                 Log.d(TAG, "Saved leak dump, the leak is still present: "
-                        + !launcher.noLeakedActivities());
+                        + !launcher.noLeakedActivities(requireOneActiveActivity));
                 sDumpWasGenerated = true;
                 result = "saved memory dump as an artifact";
             } catch (Throwable e) {
@@ -210,7 +220,8 @@
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner(this))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
-                .around(viewCaptureRule);
+                .around(viewCaptureRule)
+                .around(new TestIsolationRule(mLauncher, true));
 
         return TestHelpers.isInLauncherProcess()
                 ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
diff --git a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index e6fdbaa..2cdcf24 100644
--- a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -87,6 +87,7 @@
         MockitoAnnotations.initMocks(this);
         mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
         mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_CURSOR_HOVER_STATES);
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU);
         Utilities.enableRunningInTestHarnessForTests();
         mContext = new ActivityContextWrapper(getApplicationContext());
         mBubbleTextView = new BubbleTextView(mContext);
@@ -323,7 +324,7 @@
         mBubbleTextView.setDisplay(DISPLAY_PREDICTION_ROW);
         mBubbleTextView.applyLabel(mItemInfoWithIcon);
         mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
 
         mBubbleTextView.onPreDraw();
 
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 5417e3f..9aaca54 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -16,9 +16,6 @@
 
 package com.android.launcher3.ui;
 
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -36,23 +33,15 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.tapl.AppIcon;
-import com.android.launcher3.tapl.AppIconMenu;
-import com.android.launcher3.tapl.AppIconMenuItem;
 import com.android.launcher3.tapl.HomeAllApps;
 import com.android.launcher3.tapl.HomeAppIcon;
-import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.TISBindRule;
-import com.android.launcher3.util.rule.TestStabilityRule.Stability;
-import com.android.launcher3.widget.picker.WidgetsFullSheet;
-import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
 import org.junit.After;
 import org.junit.Before;
@@ -89,7 +78,6 @@
     public static void initialize(
             AbstractLauncherUiTest test, boolean clearWorkspace) throws Exception {
         test.reinitializeLauncherData(clearWorkspace);
-        test.mLauncher.resetFreezeRecentTaskList();
         test.mDevice.pressHome();
         test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
         test.waitForState("Launcher internal state didn't switch to Home",
@@ -97,7 +85,7 @@
         test.waitForResumed("Launcher internal state is still Background");
         // Check that we switched to home.
         test.mLauncher.getWorkspace();
-        AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher);
+        AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher, true);
     }
 
     @After
@@ -126,10 +114,6 @@
         return launcher.getWorkspace().getCurrentPage();
     }
 
-    private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
-        return WidgetsFullSheet.getWidgetsView(launcher);
-    }
-
     @Test
     public void testDevicePressMenu() throws Exception {
         mDevice.pressMenu();
@@ -212,91 +196,6 @@
         runIconLaunchFromAllAppsTest(this, allApps);
     }
 
-    @Test
-    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/293191790
-    @ScreenRecord
-    @PortraitLandscape
-    public void testWidgets() throws Exception {
-        // Test opening widgets.
-        executeOnLauncher(launcher ->
-                assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
-        Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
-        assertNotNull("openAllWidgets() returned null", widgets);
-        widgets = mLauncher.getAllWidgets();
-        assertNotNull("getAllWidgets() returned null", widgets);
-        executeOnLauncher(launcher ->
-                assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
-        executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
-                0, getWidgetsScroll(launcher)));
-
-        // Test flinging widgets.
-        widgets.flingForward();
-        Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
-        executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
-                flingForwardY > 0));
-
-        widgets.flingBackward();
-        executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
-                getWidgetsScroll(launcher) < flingForwardY));
-
-        mLauncher.goHome();
-        waitForLauncherCondition("Widgets were not closed",
-                launcher -> getWidgetsView(launcher) == null);
-    }
-
-    private int getWidgetsScroll(Launcher launcher) {
-        return getWidgetsView(launcher).computeVerticalScrollOffset();
-    }
-
-    private boolean isOptionsPopupVisible(Launcher launcher) {
-        final ArrowPopup<?> popup = launcher.getOptionsPopup();
-        return popup != null && popup.isShown();
-    }
-
-    @Test
-    @PortraitLandscape
-    @PlatinumTest(focusArea = "launcher")
-    public void testLaunchMenuItem() throws Exception {
-        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-        try {
-            final AppIconMenu menu = allApps.
-                    getAppIcon(APP_NAME).
-                    openDeepShortcutMenu();
-
-            executeOnLauncher(
-                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
-                            isOptionsPopupVisible(launcher)));
-
-            final AppIconMenuItem menuItem = menu.getMenuItem(1);
-            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
-            menuItem.launch(getAppPackageName());
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testLaunchHomeScreenMenuItem() {
-        // Drag the test app icon to home screen and open short cut menu from the icon
-        final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-        try {
-            allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false);
-            final AppIconMenu menu = mLauncher.getWorkspace().getWorkspaceAppIcon(
-                    APP_NAME).openDeepShortcutMenu();
-
-            executeOnLauncher(
-                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
-                            isOptionsPopupVisible(launcher)));
-
-            final AppIconMenuItem menuItem = menu.getMenuItem(1);
-            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
-            menuItem.launch(getAppPackageName());
-        } finally {
-            allApps.unfreeze();
-        }
-    }
     @FlakyTest(bugId = 256615483)
     @Test
     @PortraitLandscape
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 64ab206..fd4b7f1 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.ui.widget;
 
-import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static org.junit.Assert.assertNotNull;
 
 import android.platform.test.annotations.PlatinumTest;
@@ -99,6 +98,7 @@
     /**
      * Test dragging a widget to the workspace and resize it.
      */
+    @PlatinumTest(focusArea = "launcher")
     @Test
     public void testResizeWidget() throws Throwable {
         new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
new file mode 100644
index 0000000..a5e9868
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.ui.widget;
+
+import static com.android.launcher3.ui.TaplTestsLauncher3.initialize;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.tapl.Widgets;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsRecyclerView;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * This test run in both Out of process (Oop) and in-process (Ipc).
+ * Make sure the basic interactions with the WidgetPicker works.
+ */
+public class TaplWidgetPickerTest extends AbstractLauncherUiTest {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        initialize(this);
+    }
+
+    private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
+        return WidgetsFullSheet.getWidgetsView(launcher);
+    }
+
+    private int getWidgetsScroll(Launcher launcher) {
+        return getWidgetsView(launcher).computeVerticalScrollOffset();
+    }
+
+    /**
+     * Open Widget picker, make sure the widget picker can scroll and then go to home screen.
+     */
+    @Test
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/293191790
+    @ScreenRecord
+    @PortraitLandscape
+    public void testWidgets() {
+        // Test opening widgets.
+        executeOnLauncher(launcher ->
+                assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
+        Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
+        assertNotNull("openAllWidgets() returned null", widgets);
+        widgets = mLauncher.getAllWidgets();
+        assertNotNull("getAllWidgets() returned null", widgets);
+        executeOnLauncher(launcher ->
+                assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
+        executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
+                0, getWidgetsScroll(launcher)));
+
+        // Test flinging widgets.
+        widgets.flingForward();
+        Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
+        executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
+                flingForwardY > 0));
+
+        widgets.flingBackward();
+        executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
+                getWidgetsScroll(launcher) < flingForwardY));
+
+        mLauncher.goHome();
+        waitForLauncherCondition("Widgets were not closed",
+                launcher -> getWidgetsView(launcher) == null);
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/TestIsolationRule.java b/tests/src/com/android/launcher3/util/rule/TestIsolationRule.java
new file mode 100644
index 0000000..2b45902
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/TestIsolationRule.java
@@ -0,0 +1,55 @@
+/*
+ * 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.util.rule;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Isolates tests from some of the state created by the previous test.
+ */
+public class TestIsolationRule implements TestRule {
+    private final LauncherInstrumentation mLauncher;
+    private final boolean mRequireOneActiveActivity;
+
+    public TestIsolationRule(LauncherInstrumentation launcher, boolean requireOneActiveActivity) {
+        mLauncher = launcher;
+        mRequireOneActiveActivity = requireOneActiveActivity;
+    }
+
+    @NonNull
+    @Override
+    public Statement apply(@NonNull Statement base, @NonNull Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                base.evaluate();
+                // Make sure that Launcher workspace looks correct.
+
+                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressHome();
+                AbstractLauncherUiTest.checkDetectedLeaks(mLauncher, mRequireOneActiveActivity);
+            }
+        };
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 44875d5..dbb3cc3 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.tapl;
 
+import static android.view.KeyEvent.KEYCODE_META_RIGHT;
+
 import static com.android.launcher3.tapl.LauncherInstrumentation.DEFAULT_POLL_INTERVAL;
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 
@@ -42,7 +44,8 @@
 /**
  * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview.
  */
-public abstract class AllApps extends LauncherInstrumentation.VisibleContainer {
+public abstract class AllApps extends LauncherInstrumentation.VisibleContainer
+        implements KeyboardQuickSwitchSource {
     // Defer updates flag used to defer all apps updates by a test's request.
     private static final int DEFER_UPDATES_TEST = 1 << 1;
 
@@ -65,6 +68,16 @@
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public LauncherInstrumentation.ContainerType getStartingContainerType() {
+        return getContainerType();
+    }
+
     private boolean hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler,
             BySelector appIconSelector, int displayBottom) {
         final UiObject2 icon;
@@ -359,6 +372,17 @@
         }
     }
 
+    /** Presses the meta keyboard shortcut to dismiss AllApps. */
+    public void dismissByKeyboardShortcut() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT);
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "pressed meta key")) {
+                verifyVisibleContainerOnDismiss();
+            }
+        }
+    }
+
     protected abstract void verifyVisibleContainerOnDismiss();
 
     /**
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 677f204..8713b68 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -39,7 +39,8 @@
  * Indicates the base state with a UI other than Overview running as foreground. It can also
  * indicate Launcher as long as Launcher is not in Overview state.
  */
-public abstract class Background extends LauncherInstrumentation.VisibleContainer {
+public abstract class Background extends LauncherInstrumentation.VisibleContainer
+        implements KeyboardQuickSwitchSource {
     private static final int ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION = 500;
     private static final Pattern SQUARE_BUTTON_EVENT = Pattern.compile("onOverviewToggle");
 
@@ -47,6 +48,16 @@
         super(launcher);
     }
 
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public LauncherInstrumentation.ContainerType getStartingContainerType() {
+        return getContainerType();
+    }
+
     /**
      * Swipes up or presses the square button to switch to Overview.
      * Returns the base overview, which can be either in Launcher or the fallback recents.
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 252435b..85e28e8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -62,4 +62,9 @@
     protected boolean zeroButtonToOverviewGestureStateTransitionWhileHolding() {
         return true;
     }
+
+    @Override
+    public boolean isHomeState() {
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index 8542f91..d9b179c 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -113,4 +113,9 @@
     protected void verifyVisibleContainerOnDismiss() {
         mLauncher.getWorkspace();
     }
+
+    @Override
+    public boolean isHomeState() {
+        return true;
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
new file mode 100644
index 0000000..2a98a24
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
@@ -0,0 +1,212 @@
+/*
+ * 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 static com.android.launcher3.tapl.LauncherInstrumentation.KEYBOARD_QUICK_SWITCH_RES_ID;
+
+import android.view.KeyEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.testing.shared.TestProtocol;
+
+import java.util.regex.Pattern;
+
+/**
+ * Operations on the Keyboard Quick Switch View
+ */
+public final class KeyboardQuickSwitch {
+
+    private static final Pattern EVENT_ALT_TAB_DOWN = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON");
+    private static final Pattern EVENT_ALT_TAB_UP = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON");
+
+    private static final Pattern EVENT_ALT_SHIFT_TAB_DOWN = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON|META_SHIFT_ON");
+    private static final Pattern EVENT_ALT_SHIFT_TAB_UP = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON|META_SHIFT_ON");
+    private static final Pattern EVENT_ALT_ESC_DOWN = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_DOWN"
+                    + ".*?keyCode=KEYCODE_ESCAPE.*?metaState=META_ALT_ON");
+    private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_UP"
+                    + ".*?keyCode=KEYCODE_ESCAPE.*?metaState=META_ALT_ON");
+    private static final Pattern EVENT_ALT_LEFT_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP"
+                    + ".*?keyCode=KEYCODE_ALT_LEFT");
+
+    private final LauncherInstrumentation mLauncher;
+    private final LauncherInstrumentation.ContainerType mStartingContainerType;
+    private final boolean mExpectHomeKeyEventsOnDismiss;
+
+    KeyboardQuickSwitch(
+            LauncherInstrumentation launcher,
+            LauncherInstrumentation.ContainerType startingContainerType,
+            boolean expectHomeKeyEventsOnDismiss) {
+        mLauncher = launcher;
+        mStartingContainerType = startingContainerType;
+        mExpectHomeKeyEventsOnDismiss = expectHomeKeyEventsOnDismiss;
+    }
+
+    /**
+     * Focuses the next task in the Keyboard quick switch view.
+     * <p>
+     * Tasks are ordered left-to-right in LTR, and vice versa in RLT, in a carousel.
+     * <ul>
+     *      <li>If no task has been focused yet, and there is only one task, then that task will be
+     *          focused</li>
+     *      <li>If no task has been focused yet, and there are two or more tasks, then the second
+     *          task will be focused</li>
+     *      <li>If the currently-focused task is at the end of the list, the first task will be
+     *          focused</li>
+     * </ul>
+     */
+    public KeyboardQuickSwitch moveFocusForward() {
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "want to move keyboard quick switch focus forward")) {
+            mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_TAB_DOWN);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_TAB_UP);
+                mLauncher.assertTrue("Failed to press alt+tab",
+                        mLauncher.getDevice().pressKeyCode(
+                                KeyEvent.KEYCODE_TAB, KeyEvent.META_ALT_ON));
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "pressed alt+tab")) {
+                    mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+                    return this;
+                }
+            }
+        }
+    }
+
+    /**
+     * Focuses the next task in the Keyboard quick switch view.
+     * <p>
+     * Tasks are ordered left-to-right in LTR, and vice versa in RLT, in a carousel.
+     * <ul>
+     *      <li>If no task has been focused yet, and there is only one task, then that task will be
+     *          focused</li>
+     *      <li>If no task has been focused yet, and there are two or more tasks, then the second
+     *          task will be focused</li>
+     *      <li>If the currently-focused task is at the start of the list, the last task will be
+     *          focused</li>
+     * </ul>
+     */
+    public KeyboardQuickSwitch moveFocusBackward() {
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "want to move keyboard quick switch focus backward")) {
+            mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_SHIFT_TAB_DOWN);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_SHIFT_TAB_UP);
+                mLauncher.assertTrue("Failed to press alt+shift+tab",
+                        mLauncher.getDevice().pressKeyCode(
+                                KeyEvent.KEYCODE_TAB,
+                                KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON));
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "pressed alt+shift+tab")) {
+                    mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+                    return this;
+                }
+            }
+        }
+    }
+
+    /**
+     * Dismisses the Keyboard Quick Switch view without launching the focused task.
+     * <p>
+     * The device will return to the same state it started in before displaying the Keyboard Quick
+     * Switch view.
+     */
+    public void dismiss() {
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "want to dismiss keyboard quick switch view")) {
+            mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_DOWN);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP);
+                mLauncher.assertTrue("Failed to press alt+tab",
+                        mLauncher.getDevice().pressKeyCode(
+                                KeyEvent.KEYCODE_ESCAPE, KeyEvent.META_ALT_ON));
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "pressed alt+esc")) {
+                    mLauncher.waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
+                    if (mExpectHomeKeyEventsOnDismiss) {
+                        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_LEFT_UP);
+                    }
+                    mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+
+                    // Verify the final state is the same as the initial state
+                    mLauncher.verifyContainerType(mStartingContainerType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Launches the currently-focused app task.
+     * <p>
+     * This method should only be used if the focused task is for a recent running app, otherwise
+     * use {@link #launchFocusedOverviewTask()}.
+     *
+     * @param expectedPackageName the package name of the expected launched app
+     */
+    public LaunchedAppState launchFocusedAppTask(@NonNull String expectedPackageName) {
+        return (LaunchedAppState) launchFocusedTask(expectedPackageName);
+    }
+
+    /**
+     * Launches the currently-focused overview task.
+     * <p>
+     * This method only should be used if the focused task is for overview, otherwise use
+     * {@link #launchFocusedAppTask(String)}.
+     */
+    public Overview launchFocusedOverviewTask() {
+        return (Overview) launchFocusedTask(null);
+    }
+
+    private LauncherInstrumentation.VisibleContainer launchFocusedTask(
+            @Nullable String expectedPackageName) {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to launch focused task: "
+                        + (expectedPackageName == null ? "Overview" : expectedPackageName))) {
+            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+            mLauncher.waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            if (expectedPackageName != null) {
+                mLauncher.assertAppLaunched(expectedPackageName);
+                return mLauncher.getLaunchedAppState();
+            } else {
+                return mLauncher.getOverview();
+            }
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitchSource.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitchSource.java
new file mode 100644
index 0000000..b7e3d38
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitchSource.java
@@ -0,0 +1,57 @@
+/*
+ * 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 static com.android.launcher3.tapl.LauncherInstrumentation.KEYBOARD_QUICK_SWITCH_RES_ID;
+
+import android.view.KeyEvent;
+
+/**
+ * {@link com.android.launcher3.tapl.LauncherInstrumentation.VisibleContainer} that can be used to
+ * show the keyboard quick switch view.
+ */
+interface KeyboardQuickSwitchSource {
+
+    /**
+     * Shows the Keyboard Quick Switch view.
+     */
+    default KeyboardQuickSwitch showQuickSwitchView() {
+        LauncherInstrumentation launcher = getLauncher();
+
+        try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer(
+                "want to show keyboard quick switch object")) {
+            launcher.pressAndHoldKeyCode(KeyEvent.KEYCODE_TAB, KeyEvent.META_ALT_LEFT_ON);
+
+            try (LauncherInstrumentation.Closable c2 = launcher.addContextLayer(
+                    "press and held alt+tab")) {
+                launcher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+                launcher.unpressKeyCode(KeyEvent.KEYCODE_TAB, 0);
+
+                return new KeyboardQuickSwitch(
+                        launcher, getStartingContainerType(), isHomeState());
+            }
+        }
+    }
+
+    /** This method requires public access, however should not be called in tests. */
+    LauncherInstrumentation getLauncher();
+
+    /** This method requires public access, however should not be called in tests. */
+    LauncherInstrumentation.ContainerType getStartingContainerType();
+
+    /** This method requires public access, however should not be called in tests. */
+    boolean isHomeState();
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 3450ea7..f6fcfa64 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -20,15 +20,10 @@
 
 import static com.android.launcher3.testing.shared.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
 
-import android.app.UiAutomation;
 import android.graphics.Point;
 import android.view.MotionEvent;
-import android.view.accessibility.AccessibilityEvent;
 
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
@@ -57,7 +52,19 @@
      */
     public LaunchedAppState launch(String expectedPackageName) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            return launch(By.pkg(expectedPackageName));
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "want to launch an app from " + launchableType())) {
+                LauncherInstrumentation.log("Launchable.launch before click "
+                        + mObject.getVisibleCenter() + " in "
+                        + mLauncher.getVisibleBounds(mObject));
+
+                mLauncher.clickLauncherObject(mObject);
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) {
+                    expectActivityStartEvents();
+                    return mLauncher.assertAppLaunched(expectedPackageName);
+                }
+            }
         }
     }
 
@@ -65,21 +72,6 @@
 
     protected abstract String launchableType();
 
-    private LaunchedAppState launch(BySelector selector) {
-        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
-                "want to launch an app from " + launchableType())) {
-            LauncherInstrumentation.log("Launchable.launch before click "
-                    + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
-
-            mLauncher.clickLauncherObject(mObject);
-
-            try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) {
-                expectActivityStartEvents();
-                return assertAppLaunched(selector);
-            }
-        }
-    }
-
     /**
      * Clicks a launcher object to initiate splitscreen, where the selected app will be one of two
      * apps running on the screen. Should be called when Launcher is in a "split staging" state
@@ -107,14 +99,6 @@
         }
     }
 
-    protected LaunchedAppState assertAppLaunched(BySelector selector) {
-        mLauncher.assertTrue(
-                "App didn't start: (" + selector + ")",
-                mLauncher.getDevice().wait(Until.hasObject(selector),
-                        LauncherInstrumentation.WAIT_TIME_MS));
-        return new LaunchedAppState(mLauncher);
-    }
-
     Point startDrag(long downTime, Runnable expectLongClickEvents, boolean runToSpringLoadedState) {
         final Point iconCenter = getObject().getVisibleCenter();
         final Point dragStartCenter = new Point(iconCenter.x,
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 30417c0..9f8fb92 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -34,7 +34,6 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
-import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.Condition;
 import androidx.test.uiautomator.UiDevice;
 
@@ -72,6 +71,11 @@
         return LauncherInstrumentation.ContainerType.LAUNCHED_APP;
     }
 
+    @Override
+    public boolean isHomeState() {
+        return false;
+    }
+
     /**
      * Returns the taskbar.
      *
@@ -200,8 +204,8 @@
 
                     try (LauncherInstrumentation.Closable c4 = launcher.addContextLayer(
                             "dropped item")) {
-                        launchable.assertAppLaunched(By.pkg(expectedNewPackageName));
-                        launchable.assertAppLaunched(By.pkg(expectedExistingPackageName));
+                        launcher.assertAppLaunched(expectedNewPackageName);
+                        launcher.assertAppLaunched(expectedExistingPackageName);
                     }
                 }
             }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index b7d2530..d7f9c78 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -20,7 +20,10 @@
 import static android.content.pm.PackageManager.DONT_KILL_APP;
 import static android.content.pm.PackageManager.MATCH_ALL;
 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
+
 import static com.android.launcher3.tapl.Folder.FOLDER_CONTENT_RES_ID;
 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
@@ -49,6 +52,9 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
@@ -170,6 +176,7 @@
     private static final String OPEN_FOLDER_RES_ID = "folder_content";
     static final String TASKBAR_RES_ID = "taskbar_view";
     private static final String SPLIT_PLACEHOLDER_RES_ID = "split_placeholder";
+    static final String KEYBOARD_QUICK_SWITCH_RES_ID = "keyboard_quick_switch_view";
     public static final int WAIT_TIME_MS = 30000;
     static final long DEFAULT_POLL_INTERVAL = 1000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
@@ -697,6 +704,7 @@
 
     /**
      * Set the trackpad gesture type of the interaction.
+     *
      * @param trackpadGestureType whether it's not from trackpad, two-finger, three-finger, or
      *                            four-finger gesture.
      */
@@ -755,18 +763,7 @@
         return isTablet() ? getLauncherPackageName() : SYSTEMUI_PACKAGE;
     }
 
-    /**
-     * Resets the frozen recent tasks list if necessary from a previous quickswitch.
-     */
-    public void resetFreezeRecentTaskList() {
-        try {
-            mDevice.executeShellCommand("wm reset-freeze-recent-tasks");
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to reset fozen recent tasks list", e);
-        }
-    }
-
-    private UiObject2 verifyContainerType(ContainerType containerType) {
+    UiObject2 verifyContainerType(ContainerType containerType) {
         waitForLauncherInitialized();
 
         if (mExpectedRotationCheckEnabled && mExpectedRotation != null) {
@@ -795,6 +792,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -809,6 +807,7 @@
                     waitUntilLauncherObjectGone(APPS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -824,6 +823,7 @@
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     return waitForLauncherObject(APPS_RES_ID);
                 }
@@ -832,6 +832,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -852,6 +853,7 @@
                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     }
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
@@ -866,6 +868,7 @@
                     }
 
                     waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
                 case LAUNCHED_APP: {
@@ -874,6 +877,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (mIgnoreTaskbarVisibility) {
                         return null;
@@ -992,6 +996,25 @@
     }
 
     /**
+     * Goes to home from immersive fullscreen app by first swiping up to bring navbar, and then
+     * performing {@code goHome()} action.
+     * Currently only supports gesture navigation mode.
+     *
+     * @return the Workspace object.
+     */
+    public Workspace goHomeFromImmersiveFullscreenApp() {
+        assertTrue("expected gesture navigation mode",
+                getNavigationModel() == NavigationModel.ZERO_BUTTON);
+        final Point displaySize = getRealDisplaySize();
+        linearGesture(
+                displaySize.x / 2, displaySize.y - 1,
+                displaySize.x / 2, 0,
+                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
+                false, GestureScope.EXPECT_PILFER);
+        return goHome();
+    }
+
+    /**
      * Goes to home by swiping up in zero-button mode or pressing Home button.
      * Calling it after another TAPL call is safe because all TAPL methods wait for the animations
      * to finish.
@@ -1181,6 +1204,14 @@
         }
     }
 
+    LaunchedAppState assertAppLaunched(@NonNull String expectedPackageName) {
+        BySelector packageSelector = By.pkg(expectedPackageName);
+        assertTrue("App didn't start: (" + packageSelector + ")",
+                mDevice.wait(Until.hasObject(packageSelector),
+                        LauncherInstrumentation.WAIT_TIME_MS));
+        return new LaunchedAppState(this);
+    }
+
     void waitUntilLauncherObjectGone(String resId) {
         waitUntilGoneBySelector(getLauncherObjectSelector(resId));
     }
@@ -1313,6 +1344,16 @@
         }
     }
 
+    void waitForObjectFocused(UiObject2 object, String waitReason) {
+        try {
+            assertTrue("Timed out waiting for object to be focused for " + waitReason + " "
+                            + object.getResourceName(),
+                    object.wait(Until.focused(true), WAIT_TIME_MS));
+        } catch (StaleObjectException e) {
+            fail("The object disappeared from screen");
+        }
+    }
+
     @NonNull
     UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
         return waitForObjectsInContainer(container, selector).get(0);
@@ -1746,6 +1787,11 @@
                 InputDevice.SOURCE_TOUCHSCREEN);
     }
 
+    private void injectEvent(InputEvent event) {
+        assertTrue("injectInputEvent failed: event=" + event,
+                mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
+    }
+
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope, int source) {
         final boolean hasTIS = hasTIS();
@@ -1776,16 +1822,46 @@
 
         final MotionEvent event = isTrackpadGesture
                 ? getTrackpadMotionEvent(
-                        downTime, currentTime, action, point.x, point.y, pointerCount,
-                        mTrackpadGestureType)
+                downTime, currentTime, action, point.x, point.y, pointerCount,
+                mTrackpadGestureType)
                 : getMotionEvent(downTime, currentTime, action, point.x, point.y, source);
         if (action == MotionEvent.ACTION_BUTTON_PRESS
                 || action == MotionEvent.ACTION_BUTTON_RELEASE) {
             event.setActionButton(MotionEvent.BUTTON_PRIMARY);
         }
-        assertTrue("injectInputEvent failed",
-                mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
-        event.recycle();
+        injectEvent(event);
+    }
+
+    private KeyEvent createKeyEvent(int keyCode, int metaState, boolean actionDown) {
+        long eventTime = SystemClock.uptimeMillis();
+        return KeyEvent.obtain(
+                eventTime,
+                eventTime,
+                actionDown ? ACTION_DOWN : ACTION_UP,
+                keyCode,
+                /* repeat= */ 0,
+                metaState,
+                KeyCharacterMap.VIRTUAL_KEYBOARD,
+                /* scancode= */ 0,
+                /* flags= */ 0,
+                InputDevice.SOURCE_KEYBOARD,
+                /* characters =*/ null);
+    }
+
+    /**
+     * Sends a {@link KeyEvent} with {@link ACTION_DOWN} for the given key codes without sending
+     * a {@link KeyEvent} with {@link ACTION_UP}.
+     */
+    public void pressAndHoldKeyCode(int keyCode, int metaState) {
+        injectEvent(createKeyEvent(keyCode, metaState, true));
+    }
+
+
+    /**
+     * Sends a {@link KeyEvent} with {@link ACTION_UP} for the given key codes.
+     */
+    public void unpressKeyCode(int keyCode, int metaState) {
+        injectEvent(createKeyEvent(keyCode, metaState, false));
     }
 
     public long movePointer(long downTime, long startTime, long duration, Point from, Point to,
@@ -1977,6 +2053,12 @@
         getTestInfo(TestProtocol.REQUEST_RECREATE_TASKBAR);
     }
 
+    // TODO(b/270393900): Remove with ENABLE_ALL_APPS_SEARCH_IN_TASKBAR flag cleanup.
+    /** Refreshes the known overview target in TIS. */
+    public void refreshOverviewTarget() {
+        getTestInfo(TestProtocol.REQUEST_REFRESH_OVERVIEW_TARGET);
+    }
+
     public List<String> getHotseatIconNames() {
         return getTestInfo(TestProtocol.REQUEST_HOTSEAT_ICON_NAMES)
                 .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
@@ -1991,14 +2073,16 @@
         return String.join(", ", getActivities());
     }
 
-    public boolean noLeakedActivities() {
+    /** Returns whether no leaked activities are detected. */
+    public boolean noLeakedActivities(boolean requireOneActiveActivity) {
         final String[] activities = getActivities();
+
         for (String activity : activities) {
             if (activity.contains("(destroyed)")) {
                 return false;
             }
         }
-        return activities.length <= 2;
+        return activities.length <= (requireOneActiveActivity ? 1 : 2);
     }
 
     public int getActivitiesCreated() {
@@ -2136,6 +2220,8 @@
                     containerBounds.bottom,
                     getRealDisplaySize().y - getImeInsets().bottom);
             int y = (bottomBound - containerBounds.top) / 2;
+            // Do not tap in the status bar.
+            y = Math.max(y, getWindowInsets().top);
 
             final long downTime = SystemClock.uptimeMillis();
             final Point tapTarget = new Point(x, y);
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index e4cfc52..95a4802 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -168,7 +168,7 @@
         }
     }
 
-    /** Taps the task menu. */
+    /** Taps the task menu. Returns the task menu object. */
     @NonNull
     public OverviewTaskMenu tapMenu() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
@@ -184,6 +184,22 @@
         }
     }
 
+    /** Taps the task menu of the split task. Returns the split task's menu object. */
+    @NonNull
+    public OverviewTaskMenu tapSplitTaskMenu() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to tap the split task's menu")) {
+            mLauncher.clickLauncherObject(
+                    mLauncher.waitForObjectInContainer(mTask.getParent(), "bottomRight_icon"));
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "tapped the split task's menu")) {
+                return new OverviewTaskMenu(mLauncher);
+            }
+        }
+    }
+
     boolean isTaskSplit() {
         return mLauncher.findObjectInContainer(mTask.getParent(), "bottomright_snapshot") != null;
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index 859e504..25c73de 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -91,4 +91,11 @@
         return new OverviewTaskMenuItem(mLauncher,
                 mLauncher.waitForObjectInContainer(mMenu, By.text(menuItemName)));
     }
+
+    /**
+     * Taps outside task menu to dismiss it.
+     */
+    public void touchOutsideTaskMenuToDismiss() {
+        mLauncher.touchOutsideContainer(mMenu, false);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Qsb.java b/tests/tapl/com/android/launcher3/tapl/Qsb.java
index 0f2aff8..5ca80a3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Qsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/Qsb.java
@@ -25,7 +25,7 @@
 /**
  * Operations on qsb from either Home screen or AllApp screen.
  */
-public abstract class Qsb {
+public abstract class Qsb implements SearchInputSource {
 
     private static final String ASSISTANT_APP_PACKAGE = "com.google.android.googlequicksearchbox";
     private static final String ASSISTANT_ICON_RES_ID = "mic_icon";
@@ -125,6 +125,16 @@
         }
     }
 
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public SearchResultFromQsb getSearchResultForInput() {
+        return createSearchResult();
+    }
+
     protected SearchResultFromQsb createSearchResult() {
         return new SearchResultFromQsb(mLauncher);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchInputSource.java b/tests/tapl/com/android/launcher3/tapl/SearchInputSource.java
new file mode 100644
index 0000000..032948f
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SearchInputSource.java
@@ -0,0 +1,52 @@
+/*
+ * 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 androidx.test.uiautomator.UiObject2;
+
+import com.android.launcher3.testing.shared.TestProtocol;
+
+/**
+ * Container that can be used to input a search query and retrieve a {@link SearchResultFromQsb}
+ * instance.
+ */
+interface SearchInputSource {
+    String INPUT_RES = "input";
+
+    /** Set the already focused search input edit text and update search results. */
+    default SearchResultFromQsb searchForInput(String input) {
+        LauncherInstrumentation launcher = getLauncher();
+        try (LauncherInstrumentation.Closable c = launcher.addContextLayer(
+                "want to search for result with an input");
+             LauncherInstrumentation.Closable e = launcher.eventsCheck()) {
+            launcher.executeAndWaitForLauncherEvent(
+                    () -> {
+                        UiObject2 editText = launcher.waitForLauncherObject(INPUT_RES);
+                        launcher.waitForObjectFocused(editText, "search input");
+                        editText.setText(input);
+                    },
+                    event -> TestProtocol.SEARCH_RESULT_COMPLETE.equals(event.getClassName()),
+                    () -> "Didn't receive a search result completed message", "searching");
+            return getSearchResultForInput();
+        }
+    }
+
+    /** This method requires public access, however should not be called in tests. */
+    LauncherInstrumentation getLauncher();
+
+    /** This method requires public access, however should not be called in tests. */
+    SearchResultFromQsb getSearchResultForInput();
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
index 513d6bb..f0a8aa2 100644
--- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
@@ -20,16 +20,12 @@
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
 
-import com.android.launcher3.testing.shared.TestProtocol;
-
 import java.util.ArrayList;
 
 /**
  * Operations on search result page opened from qsb.
  */
-public class SearchResultFromQsb {
-    // The input resource id in the search box.
-    private static final String INPUT_RES = "input";
+public class SearchResultFromQsb implements SearchInputSource {
     private static final String BOTTOM_SHEET_RES_ID = "bottom_sheet_background";
 
     // This particular ID change should happen with caution
@@ -41,18 +37,6 @@
         mLauncher.waitForLauncherObject("search_container_all_apps");
     }
 
-    /** Set the input to the search input edit text and update search results. */
-    public void searchForInput(String input) {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to search for result with an input");
-            LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            mLauncher.executeAndWaitForLauncherEvent(
-                    () -> mLauncher.waitForLauncherObject(INPUT_RES).setText(input),
-                    event -> TestProtocol.SEARCH_RESULT_COMPLETE.equals(event.getClassName()),
-                    () -> "Didn't receive a search result completed message", "searching");
-        }
-    }
-
     /** Find the app from search results with app name. */
     public AppIcon findAppIcon(String appName) {
         UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName));
@@ -114,4 +98,14 @@
     protected void verifyVisibleContainerOnDismiss() {
         mLauncher.getWorkspace();
     }
+
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public SearchResultFromQsb getSearchResultForInput() {
+        return this;
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java b/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java
index ce1c3c0..2870877 100644
--- a/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java
+++ b/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.tapl;
 
-/** Launchable that can serve as a source for dragging and dropping to splitscreen. */
+/** {@link Launchable} that can serve as a source for dragging and dropping to splitscreen. */
 interface SplitscreenDragSource {
 
     /**
@@ -35,5 +35,6 @@
         }
     }
 
+    /** This method requires public access, however should not be called in tests. */
     Launchable getLaunchable();
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index 4293ee8..da26694 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.tapl;
 
+import static android.view.KeyEvent.KEYCODE_META_RIGHT;
+
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_DISABLE_MANUAL_TASKBAR_STASHING;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING;
@@ -105,6 +107,17 @@
         }
     }
 
+    /** Opens the Taskbar all apps page with the meta keyboard shortcut. */
+    public TaskbarAllApps openAllAppsFromKeyboardShortcut() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT);
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "pressed meta key")) {
+                return getAllApps();
+            }
+        }
+    }
+
     /** Returns {@link TaskbarAllApps} if it is open, otherwise fails. */
     public TaskbarAllApps getAllApps() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java
index c1234fe..3d39041 100644
--- a/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java
@@ -73,4 +73,9 @@
     protected void verifyVisibleContainerOnDismiss() {
         mLauncher.getLaunchedAppState().assertTaskbarVisible();
     }
+
+    @Override
+    public boolean isHomeState() {
+        return false;
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 79b54ba..105bc3b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -34,7 +34,8 @@
 /**
  * All widgets container.
  */
-public final class Widgets extends LauncherInstrumentation.VisibleContainer {
+public final class Widgets extends LauncherInstrumentation.VisibleContainer
+        implements KeyboardQuickSwitchSource {
     private static final int FLING_STEPS = 10;
     private static final int SCROLL_ATTEMPTS = 60;
 
@@ -43,6 +44,21 @@
         verifyActiveContainer();
     }
 
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public LauncherInstrumentation.ContainerType getStartingContainerType() {
+        return getContainerType();
+    }
+
+    @Override
+    public boolean isHomeState() {
+        return true;
+    }
+
     /**
      * Flings forward (down) and waits the fling's end.
      */
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index cf48ebc..fc589bd 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.tapl;
 
+import static android.view.KeyEvent.KEYCODE_META_RIGHT;
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED;
 
 import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL;
@@ -115,6 +116,20 @@
         }
     }
 
+    /** Opens the Launcher all apps page with the meta keyboard shortcut. */
+    public HomeAllApps openAllAppsFromKeyboardShortcut() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to open all apps search")) {
+            verifyActiveContainer();
+            mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "pressed meta key")) {
+                return new HomeAllApps(mLauncher);
+            }
+        }
+    }
+
     /**
      * Returns the home qsb.
      *
diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
index 141476c..5a4d562 100644
--- a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
+++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
@@ -19,7 +19,7 @@
 
 import java.util.function.Supplier;
 
-/** Launchable that can serve as a source for dragging and dropping to the workspace. */
+/** {@link Launchable} that can serve as a source for dragging and dropping to the workspace. */
 interface WorkspaceDragSource {
 
     /**