Merge "Add CUJ annotations for going to Recents from Home" into main
diff --git a/aconfig/Android.bp b/aconfig/Android.bp
index dc30a35..5413601 100644
--- a/aconfig/Android.bp
+++ b/aconfig/Android.bp
@@ -20,6 +20,7 @@
 aconfig_declarations {
     name: "com_android_launcher3_flags",
     package: "com.android.launcher3",
+    container: "system",
     srcs: ["**/*.aconfig"],
 }
 
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 82ae4cb..6d899d9 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.launcher3"
+container: "system"
 
 flag {
     name: "enable_expanding_pause_work_button"
diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index 4e16e7f..2f2690e 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.launcher3"
+container: "system"
 
 flag {
     name: "enable_private_space"
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index b7acb70..fb9bf99 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -17,45 +17,41 @@
 <com.android.quickstep.views.IconAppChipView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/icon"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
+    android:layout_width="@dimen/task_thumbnail_icon_menu_expanded_width"
+    android:layout_height="@dimen/task_thumbnail_icon_menu_expanded_height"
+    android:clipToOutline="true"
     android:focusable="false"
     android:importantForAccessibility="no"
     android:autoMirrored="true"
-    android:background="@drawable/icon_menu_elevation_background"
-    android:elevation="@dimen/task_thumbnail_icon_menu_elevation" >
+    android:elevation="@dimen/task_thumbnail_icon_menu_elevation"
+    android:background="?androidprv:attr/materialColorSurfaceBright">
 
-    <ImageView
-        android:id="@+id/icon_view_background_corners_start"
-        android:layout_width="@dimen/task_thumbnail_icon_menu_corner_width"
-        android:layout_height="@dimen/task_thumbnail_icon_menu_min_height"
-        android:src="@drawable/icon_menu_background_corners"
-        android:importantForAccessibility="no" />
-    <ImageView
-        android:id="@+id/icon_view_background"
-        android:layout_width="@dimen/task_thumbnail_icon_menu_background_min_width"
-        android:layout_height="@dimen/task_thumbnail_icon_menu_min_height"
-        android:src="@drawable/icon_menu_background"
-        android:importantForAccessibility="no" />
-    <ImageView
-        android:id="@+id/icon_view_background_corners_end"
-        android:layout_width="@dimen/task_thumbnail_icon_menu_corner_width"
-        android:layout_height="@dimen/task_thumbnail_icon_menu_min_height"
-        android:src="@drawable/icon_menu_background_corners"
-        android:importantForAccessibility="no" />
+    <!-- ignoring warning because the user of the anchor is a Rect where RTL is not needed -->
+    <!-- This anchor's bounds is in the expected location after rotations and translations are
+    applied to the parent. The same is not true of the parent so an anchor is used. -->
+    <!-- marginTop is applied in java to get the gap between chip and menu -->
+    <View
+        android:id="@+id/icon_view_menu_anchor"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_gravity="left|top"
+        android:focusable="false"
+        android:importantForAccessibility="no"
+        tools:ignore="RtlHardcoded" />
 
     <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:layout_width="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+        android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
         android:focusable="false"
         android:importantForAccessibility="no" />
 
     <TextView
         android:id="@+id/icon_text_collapsed"
-        android:layout_width="@dimen/task_thumbnail_icon_menu_text_width"
-        android:layout_height="@dimen/task_thumbnail_icon_menu_drawable_size"
+        android:layout_width="@dimen/task_thumbnail_icon_menu_text_collapsed_max_width"
+        android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
         android:gravity="start|center_vertical"
         android:maxLines="1"
         android:ellipsize="end"
@@ -65,8 +61,8 @@
 
     <TextView
         android:id="@+id/icon_text_expanded"
-        android:layout_width="@dimen/task_thumbnail_icon_menu_text_max_width"
-        android:layout_height="@dimen/task_thumbnail_icon_menu_drawable_size"
+        android:layout_width="@dimen/task_thumbnail_icon_menu_text_expanded_max_width"
+        android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
         android:gravity="start|center_vertical"
         android:maxLines="1"
         android:ellipsize="end"
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index a985cd8..853ac74 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -32,8 +32,11 @@
     <dimen name="overview_minimum_next_prev_size">50dp</dimen>
 
     <!--  Overview Task Views  -->
-    <!--  The primary task thumbnail uses up to this much of the total screen height/width  -->
+    <!--  The thumbnail uses up to this much of the total screen height/width in Overview -->
     <item name="overview_max_scale" format="float" type="dimen">0.7</item>
+    <!--  The thumbnail should not go smaller than this much of the total screen height/width in
+             tablet app to Overview carousel -->
+    <item name="overview_carousel_min_scale" format="float" type="dimen">0.46</item>
     <!--  A touch target for icons, sometimes slightly larger than the icons themselves  -->
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <!--  The icon size for the focused task, placed in center of touch target  -->
@@ -44,48 +47,38 @@
     <dimen name="overview_task_margin">16dp</dimen>
     <!--  The horizontal space between tasks  -->
     <dimen name="overview_page_spacing">16dp</dimen>
-    <!--  The min width of the thumbnail icon menu for non-split tasks  -->
-    <dimen name="task_thumbnail_icon_menu_min_width">156dp</dimen>
-    <!--  The max width of the thumbnail icon menu  -->
-    <dimen name="task_thumbnail_icon_menu_max_width">216dp</dimen>
-    <!--  The width of the thumbnail icon menu background  -->
-    <dimen name="task_thumbnail_icon_menu_background_min_width">120dp</dimen>
-    <!--  The width of the icon menu text  -->
-    <dimen name="task_thumbnail_icon_menu_text_width">86dp</dimen>
-    <!--  The max width of the icon menu text  -->
-    <dimen name="task_thumbnail_icon_menu_text_max_width">118dp</dimen>
+    <!--  The collapsed max width of the icon menu text  -->
+    <dimen name="task_thumbnail_icon_menu_text_collapsed_max_width">86dp</dimen>
+    <!--  The expanded max width of the icon menu text  -->
+    <dimen name="task_thumbnail_icon_menu_text_expanded_max_width">118dp</dimen>
     <!--  The size of the icon menu text  -->
     <dimen name="task_thumbnail_icon_menu_text_size">14sp</dimen>
-    <!--  The max width of the thumbnail icon menu background  -->
-    <dimen name="task_thumbnail_icon_menu_background_max_width">164dp</dimen>
-    <!--  The height of the thumbnail icon menu  -->
-    <dimen name="task_thumbnail_icon_menu_min_height">36dp</dimen>
-    <!--  The corner radius of the thumbnail icon menu  -->
-    <dimen name="task_thumbnail_icon_menu_corner_radius">28dp</dimen>
-    <!--  The width of the thumbnail icon menu backgorund's corners when collapsed  -->
-    <dimen name="task_thumbnail_icon_menu_corner_width">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">24dp</dimen>
-    <!--  The size of the icon menu arrow drawable  -->
-    <dimen name="task_thumbnail_icon_menu_arrow_drawable_size">16dp</dimen>
-    <!--  The margin at the start of the task icon menu  -->
-    <dimen name="task_thumbnail_icon_menu_start_margin">12dp</dimen>
-    <!--  The margin at the top of the task icon menu  -->
-    <dimen name="task_thumbnail_icon_menu_top_margin">12dp</dimen>
+    <!--  The width of the thumbnail icon menu when collapsed (for non-split tasks)  -->
+    <dimen name="task_thumbnail_icon_menu_collapsed_width">156dp</dimen>
+    <!--  The width of the thumbnail icon menu when expanded -->
+    <dimen name="task_thumbnail_icon_menu_expanded_width">216dp</dimen>
+    <!--  The height of the thumbnail icon menu when collapsed  -->
+    <dimen name="task_thumbnail_icon_menu_collapsed_height">36dp</dimen>
+    <!--  The height of the thumbnail icon menu when expanded -->
+    <dimen name="task_thumbnail_icon_menu_expanded_height">52dp</dimen>
+    <!--  The margin at the top/start of the task icon menu when expanded  -->
+    <dimen name="task_thumbnail_icon_menu_expanded_top_start_margin">4dp</dimen>
+    <!--  The margin at the start of the background when collapsed  -->
+    <dimen name="task_thumbnail_icon_menu_background_margin_top_start">8dp</dimen>
+    <!--  The margin between the app name + app icon and app name + arrow icon when collapsed  -->
+    <dimen name="task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed">8dp</dimen>
     <!--  The gap at the top of the task icon menu when expanded  -->
-    <dimen name="task_thumbnail_icon_menu_expanded_gap">6dp</dimen>
+    <dimen name="task_thumbnail_icon_menu_expanded_gap">2dp</dimen>
     <!--  The margin at the start of the task icon view in the icon menu  -->
     <dimen name="task_thumbnail_icon_view_start_margin">6dp</dimen>
     <!--  The space around the task icon arrow within the icon menu  -->
     <dimen name="task_thumbnail_icon_menu_arrow_margin">8dp</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 icon size for the icon menu  -->
-    <dimen name="task_thumbnail_icon_menu_drawable_max_size">32dp</dimen>
+    <!--  The size for the icon menu arrow -->
+    <dimen name="task_thumbnail_icon_menu_arrow_size">24dp</dimen>
+    <!--  The collapsed size for the icon menu icon -->
+    <dimen name="task_thumbnail_icon_menu_app_icon_collapsed_size">24dp</dimen>
+    <!--  The expanded icon size for the icon menu -->
+    <dimen name="task_thumbnail_icon_menu_app_icon_expanded_size">32dp</dimen>
     <!--  The size of the icon menu's icon touch target  -->
     <dimen name="task_thumbnail_icon_menu_drawable_touch_size">44dp</dimen>
     <dimen name="task_thumbnail_icon_menu_elevation">4dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 436fe3b..68e7824 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ClipData;
 import android.content.ClipDescription;
@@ -50,7 +51,6 @@
 /** An Activity that can host Launcher's widget picker. */
 public class WidgetPickerActivity extends BaseActivity {
     private static final String TAG = "WidgetPickerActivity";
-    private static final boolean DEBUG = false;
 
     /**
      * Name of the extra that indicates that a widget being dragged.
@@ -65,13 +65,13 @@
     private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
     private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
 
-
     private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
     private WidgetsModel mModel;
     private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
 
     private int mDesiredWidgetWidth;
     private int mDesiredWidgetHeight;
+    private int mWidgetCategoryFilter;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -104,6 +104,10 @@
         mDesiredWidgetHeight =
                 getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
 
+        // Defaults to '0' to indicate that there isn't a category filter.
+        mWidgetCategoryFilter =
+                getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+
         refreshAndBindWidgets();
     }
 
@@ -199,6 +203,14 @@
             return rejectWidget(widget, "shortcut");
         }
 
+        if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
+            return rejectWidget(
+                    widget,
+                    "doesn't match category filter [filter=%d, widget=%d]",
+                    mWidgetCategoryFilter,
+                    info.widgetCategory);
+        }
+
         if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
             // Accept the widget if the desired dimensions are unspecified.
             return acceptWidget(widget);
@@ -210,22 +222,18 @@
             if (info.maxResizeWidth > 0 && info.maxResizeWidth < mDesiredWidgetWidth) {
                 return rejectWidget(
                         widget,
-                        String.format(
-                                Locale.ENGLISH,
-                                "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
-                                info.maxResizeWidth,
-                                mDesiredWidgetWidth));
+                        "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
+                        info.maxResizeWidth,
+                        mDesiredWidgetWidth);
             }
 
             final int minWidth = info.minResizeWidth > 0 ? info.minResizeWidth : info.minWidth;
             if (minWidth > mDesiredWidgetWidth) {
                 return rejectWidget(
                         widget,
-                        String.format(
-                                Locale.ENGLISH,
-                                "minWidth[%d] > mDesiredWidgetWidth[%d]",
-                                minWidth,
-                                mDesiredWidgetWidth));
+                        "minWidth[%d] > mDesiredWidgetWidth[%d]",
+                        minWidth,
+                        mDesiredWidgetWidth);
             }
         }
 
@@ -235,22 +243,18 @@
             if (info.maxResizeHeight > 0 && info.maxResizeHeight < mDesiredWidgetHeight) {
                 return rejectWidget(
                         widget,
-                        String.format(
-                                Locale.ENGLISH,
-                                "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
-                                info.maxResizeHeight,
-                                mDesiredWidgetHeight));
+                        "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
+                        info.maxResizeHeight,
+                        mDesiredWidgetHeight);
             }
 
             final int minHeight = info.minResizeHeight > 0 ? info.minResizeHeight : info.minHeight;
             if (minHeight > mDesiredWidgetHeight) {
                 return rejectWidget(
                         widget,
-                        String.format(
-                                Locale.ENGLISH,
-                                "minHeight[%d] > mDesiredWidgetHeight[%d]",
-                                minHeight,
-                                mDesiredWidgetHeight));
+                        "minHeight[%d] > mDesiredWidgetHeight[%d]",
+                        minHeight,
+                        mDesiredWidgetHeight);
             }
         }
 
@@ -264,8 +268,11 @@
     }
 
     private static WidgetAcceptabilityVerdict rejectWidget(
-            WidgetItem widget, String rejectionReason) {
-        return new WidgetAcceptabilityVerdict(false, widget.label, rejectionReason);
+            WidgetItem widget, String rejectionReason, Object... args) {
+        return new WidgetAcceptabilityVerdict(
+                false,
+                widget.label,
+                String.format(Locale.ENGLISH, rejectionReason, args));
     }
 
     private static WidgetAcceptabilityVerdict acceptWidget(WidgetItem widget) {
@@ -276,7 +283,7 @@
             boolean isAcceptable, String widgetLabel, String reason) {
         void maybeLogVerdict() {
             // Only log a verdict if a reason is specified.
-            if (DEBUG && !reason.isEmpty()) {
+            if (Log.isLoggable(TAG, Log.DEBUG) && !reason.isEmpty()) {
                 Log.i(TAG, String.format(
                         Locale.ENGLISH,
                         "%s: %s because %s",
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 3f5793f..55deca8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1063,7 +1063,7 @@
                         Toast.LENGTH_SHORT).show();
             } else {
                 // Else launch the selected app pair
-                launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents);
+                launchFromTaskbar(recents, view, fi.contents);
                 mControllers.uiController.onTaskbarIconLaunched(fi);
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
@@ -1097,8 +1097,7 @@
                             getSystemService(LauncherApps.class)
                                     .startShortcut(packageName, id, null, null, info.user);
                         } else {
-                            launchFromTaskbarPreservingSplitIfVisible(
-                                    recents, view, Collections.singletonList(info));
+                            launchFromTaskbar(recents, view, Collections.singletonList(info));
                         }
 
                     } catch (NullPointerException
@@ -1136,8 +1135,7 @@
                 // If we are selecting a second app for split, launch the split tasks
                 taskbarUIController.triggerSecondAppForSplit(info, info.intent, view);
             } else {
-                launchFromTaskbarPreservingSplitIfVisible(
-                        recents, view, Collections.singletonList(info));
+                launchFromTaskbar(recents, view, Collections.singletonList(info));
             }
             mControllers.uiController.onTaskbarIconLaunched(info);
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1153,12 +1151,48 @@
     }
 
     /**
+     * Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app),
+     * and calls the appropriate method to animate and launch.
+     */
+    private void launchFromTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView,
+            List<? extends ItemInfo> itemInfos) {
+        if (isInApp()) {
+            launchFromInAppTaskbar(recents, launchingIconView, itemInfos);
+        } else {
+            launchFromOverviewTaskbar(recents, launchingIconView, itemInfos);
+        }
+    }
+
+    /**
+     * Runs when the user taps a Taskbar icon while inside an app.
+     */
+    private void launchFromInAppTaskbar(@Nullable RecentsView recents,
+            @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
+        if (recents == null) {
+            return;
+        }
+
+        boolean tappedAppPair = itemInfos.size() == 2;
+
+        if (tappedAppPair) {
+            // If the icon is an app pair, the logic gets a bit complicated because we play
+            // different animations depending on which app (or app pair) is currently running on
+            // screen, so delegate logic to appPairsController.
+            recents.getSplitSelectController().getAppPairsController()
+                    .handleAppPairLaunchInApp((AppPairIcon) launchingIconView, itemInfos);
+        } else {
+            // Tapped a single app, nothing complicated here.
+            startItemInfoActivity(itemInfos.get(0));
+        }
+    }
+
+    /**
      * Run when the user taps a Taskbar icon while in Overview. If the tapped app is currently
      * visible to the user in Overview, or is part of a visible split pair, we expand the TaskView
      * as if the user tapped on it (preserving the split pair). Otherwise, launch it normally
      * (potentially breaking a split pair).
      */
-    private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents,
+    private void launchFromOverviewTaskbar(@Nullable RecentsView recents,
             @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
         if (recents == null) {
             return;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 9f6994a..8d83716 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -222,8 +222,7 @@
     public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
         // TODO(b/279514548) Cleans up bad state that can occur when user interacts with
         // taskbar on top of transparent activity.
-        if (!FeatureFlags.enableHomeTransitionListener()
-                && (finalState == LauncherState.NORMAL)
+        if ((finalState == LauncherState.NORMAL)
                 && mLauncher.hasBeenResumed()) {
             updateStateForFlag(FLAG_VISIBLE, true);
             applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index db7d0eb..7a69c55 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -21,6 +21,7 @@
 import static com.android.app.animation.Interpolators.FINAL_FRAME;
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.internal.jank.InteractionJankMonitor.Configuration;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
@@ -572,7 +573,8 @@
             mAnimator.cancel();
         }
         mAnimator = new AnimatorSet();
-        addJankMonitorListener(mAnimator, /* appearing= */ !mIsStashed);
+        addJankMonitorListener(
+                mAnimator, /* expanding= */ !mIsStashed, /* animationType= */ animationType);
         boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
         final float stashTranslation = mActivity.isPhoneMode() || isTransientTaskbar
                 ? 0
@@ -798,14 +800,20 @@
         as.play(a);
     }
 
-    private void addJankMonitorListener(AnimatorSet animator, boolean expanding) {
+    private void addJankMonitorListener(
+            AnimatorSet animator, boolean expanding, @StashAnimation int animationType) {
         View v = mControllers.taskbarActivityContext.getDragLayer();
         int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND :
                 InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE;
         animator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
-                InteractionJankMonitor.getInstance().begin(v, action);
+                final Configuration.Builder builder =
+                        Configuration.Builder.withView(action, v);
+                if (animationType == TRANSITION_HOME_TO_APP) {
+                    builder.setTag("HOME_TO_APP");
+                }
+                InteractionJankMonitor.getInstance().begin(builder);
             }
 
             @Override
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 6698600..4752225 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.BaseActivity.EVENT_STARTED;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
 import static com.android.launcher3.PagedView.INVALID_PAGE;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
@@ -2561,9 +2562,11 @@
         }
 
         float scrollOffset = Math.abs(mRecentsView.getScrollOffset(mRecentsView.getCurrentPage()));
+        Rect carouselTaskSize = enableGridOnlyOverview()
+                ? mRecentsView.getLastComputedCarouselTaskSize()
+                : mRecentsView.getLastComputedTaskSize();
         int maxScrollOffset = mRecentsView.getPagedOrientationHandler().getPrimaryValue(
-                mRecentsView.getLastComputedTaskSize().width(),
-                mRecentsView.getLastComputedTaskSize().height());
+                carouselTaskSize.width(), carouselTaskSize.height());
         maxScrollOffset += mRecentsView.getPageSpacing();
 
         float maxScaleProgress =
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index b89d20c..879312d 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -261,6 +261,23 @@
         }
     }
 
+    /**
+     * Calculates the taskView size for carousel during app to overview animation on tablets.
+     */
+    public final void calculateCarouselTaskSize(Context context, DeviceProfile dp, Rect outRect,
+            PagedOrientationHandler orientedState) {
+        if (dp.isTablet && dp.isGestureMode) {
+            Resources res = context.getResources();
+            float minScale = res.getFloat(R.dimen.overview_carousel_min_scale);
+            Rect gridRect = new Rect();
+            calculateGridSize(dp, context, gridRect);
+            calculateTaskSizeInternal(context, dp, gridRect, minScale, Gravity.CENTER | Gravity.TOP,
+                    outRect);
+        } else {
+            calculateTaskSize(context, dp, outRect, orientedState);
+        }
+    }
+
     private void calculateFocusTaskSize(Context context, DeviceProfile dp, Rect outRect) {
         Resources res = context.getResources();
         float maxScale = res.getFloat(R.dimen.overview_max_scale);
@@ -286,13 +303,13 @@
     }
 
     private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
-            Rect potentialTaskRect, float maxScale, int gravity, Rect outRect) {
+            Rect potentialTaskRect, float targetScale, int gravity, Rect outRect) {
         PointF taskDimension = getTaskDimension(context, dp);
 
         float scale = Math.min(
                 potentialTaskRect.width() / taskDimension.x,
                 potentialTaskRect.height() / taskDimension.y);
-        scale = Math.min(scale, maxScale);
+        scale = Math.min(scale, targetScale);
         int outWidth = Math.round(scale * taskDimension.x);
         int outHeight = Math.round(scale * taskDimension.y);
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index d617828..cb0aa8f 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
 import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED;
@@ -54,15 +53,11 @@
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.provider.Settings;
-import android.util.Log;
-import android.view.ISystemGestureExclusionListener;
-import android.view.IWindowManager;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
-import android.view.WindowManagerGlobal;
 
-import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.config.FeatureFlags;
@@ -73,6 +68,8 @@
 import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.GestureExclusionManager;
+import com.android.quickstep.util.GestureExclusionManager.ExclusionListener;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -86,7 +83,7 @@
 /**
  * Manages the state of the system during a swipe up gesture.
  */
-public class RecentsAnimationDeviceState implements DisplayInfoChangeListener {
+public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener {
 
     private static final String TAG = "RecentsAnimationDeviceState";
 
@@ -99,25 +96,9 @@
     private final Context mContext;
     private final DisplayController mDisplayController;
 
-    private final IWindowManager mIWindowManager;
+    private final GestureExclusionManager mExclusionManager;
 
-    @VisibleForTesting
-    final ISystemGestureExclusionListener mGestureExclusionListener =
-            new ISystemGestureExclusionListener.Stub() {
-                @BinderThread
-                @Override
-                public void onSystemGestureExclusionChanged(int displayId,
-                        Region systemGestureExclusionRegion, Region unrestrictedOrNull) {
-                    if (displayId != DEFAULT_DISPLAY) {
-                        return;
-                    }
-                    // Assignments are atomic, it should be safe on binder thread. Also we don't
-                    // think systemGestureExclusionRegion can be null but just in case, don't
-                    // let mExclusionRegion be null.
-                    mExclusionRegion = systemGestureExclusionRegion != null
-                            ? systemGestureExclusionRegion : new Region();
-                }
-            };
+
     private final RotationTouchHelper mRotationTouchHelper;
     private final TaskStackChangeListener mPipListener;
     // Cache for better performance since it doesn't change at runtime.
@@ -140,20 +121,20 @@
     private boolean mPipIsActive;
 
     private int mGestureBlockingTaskId = -1;
-    private @NonNull Region mExclusionRegion = new Region();
+    private @NonNull Region mExclusionRegion = GestureExclusionManager.EMPTY_REGION;
     private boolean mExclusionListenerRegistered;
 
     public RecentsAnimationDeviceState(Context context) {
-        this(context, false, WindowManagerGlobal.getWindowManagerService());
+        this(context, false, GestureExclusionManager.INSTANCE);
     }
 
     public RecentsAnimationDeviceState(Context context, boolean isInstanceForTouches) {
-        this(context, isInstanceForTouches, WindowManagerGlobal.getWindowManagerService());
+        this(context, isInstanceForTouches, GestureExclusionManager.INSTANCE);
     }
 
     @VisibleForTesting
-    RecentsAnimationDeviceState(Context context, IWindowManager windowManager) {
-        this(context, false, windowManager);
+    RecentsAnimationDeviceState(Context context, GestureExclusionManager exclusionManager) {
+        this(context, false, exclusionManager);
     }
 
     /**
@@ -162,10 +143,10 @@
      */
     RecentsAnimationDeviceState(
             Context context, boolean isInstanceForTouches,
-            IWindowManager windowManager) {
+            GestureExclusionManager exclusionManager) {
         mContext = context;
         mDisplayController = DisplayController.INSTANCE.get(context);
-        mIWindowManager = windowManager;
+        mExclusionManager = exclusionManager;
         mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
         mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
         if (isInstanceForTouches) {
@@ -276,43 +257,33 @@
         }
     }
 
-    /**
-     * Registers {@link mGestureExclusionListener} for getting exclusion rect changes. Note that we
-     * make binder call on {@link UI_HELPER_EXECUTOR} to avoid jank.
-     */
-    public void registerExclusionListener() {
-        UI_HELPER_EXECUTOR.execute(() -> {
-            if (mExclusionListenerRegistered) {
-                return;
-            }
-            try {
-                mIWindowManager.registerSystemGestureExclusionListener(
-                        mGestureExclusionListener, DEFAULT_DISPLAY);
-                mExclusionListenerRegistered = true;
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to register window manager callbacks", e);
-            }
-        });
+    @Override
+    public void onGestureExclusionChanged(@Nullable Region exclusionRegion,
+            @Nullable Region unrestrictedOrNull) {
+        mExclusionRegion = exclusionRegion != null
+                ? exclusionRegion : GestureExclusionManager.EMPTY_REGION;
     }
 
     /**
-     * Unregisters {@link mGestureExclusionListener} if previously registered. We make binder call
-     * on same {@link UI_HELPER_EXECUTOR} as in {@link #registerExclusionListener()} so that
-     * read/write {@link mExclusionListenerRegistered} field is thread safe.
+     * Registers itself for getting exclusion rect changes.
+     */
+    public void registerExclusionListener() {
+        if (mExclusionListenerRegistered) {
+            return;
+        }
+        mExclusionManager.addListener(this);
+        mExclusionListenerRegistered = true;
+    }
+
+    /**
+     * Unregisters itself as gesture exclusion listener if previously registered.
      */
     public void unregisterExclusionListener() {
-        UI_HELPER_EXECUTOR.execute(() -> {
-            if (!mExclusionListenerRegistered) {
-                return;
-            }
-            try {
-                mIWindowManager.unregisterSystemGestureExclusionListener(
-                        mGestureExclusionListener, DEFAULT_DISPLAY);
-                mExclusionListenerRegistered = false;
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to unregister window manager callbacks", e);
-            }
-        });
+        if (!mExclusionListenerRegistered) {
+            return;
+        }
+        mExclusionManager.removeListener(this);
+        mExclusionListenerRegistered = false;
     }
 
     public void onOneHandedModeChanged(int newGesturalHeight) {
@@ -515,10 +486,8 @@
      *         This is only used for quickswitch, and not swipe up.
      */
     public boolean isInExclusionRegion(MotionEvent event) {
-        // mExclusionRegion can change on binder thread, use a local instance here.
-        Region exclusionRegion = mExclusionRegion;
         return mMode == NO_BUTTON
-                && exclusionRegion.contains((int) event.getX(), (int) event.getY());
+                && mExclusionRegion.contains((int) event.getX(), (int) event.getY());
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
index a7ca515..8648b56 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
@@ -274,21 +274,12 @@
     @Override
     public float getTaskMenuX(float x, View thumbnailView,
             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 taskViewIcon) {
-        if (enableOverviewIconMenu()) {
-            return y - (thumbnailView.getLayoutDirection() == LAYOUT_DIRECTION_RTL
-                    ? taskMenuView.getMeasuredHeight() * 2 - (taskInsetMargin / 2f)
-                    : taskMenuView.getMeasuredHeight());
-
-        }
         BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
         int taskMenuWidth = lp.width;
         if (stagePosition == STAGE_POSITION_UNDEFINED) {
@@ -304,7 +295,7 @@
             @StagePosition int stagePosition) {
         if (enableOverviewIconMenu()) {
             return thumbnailView.getResources().getDimensionPixelSize(
-                    R.dimen.task_thumbnail_icon_menu_max_width);
+                    R.dimen.task_thumbnail_icon_menu_expanded_width);
         }
         if (stagePosition == SplitConfigurationOptions.STAGE_POSITION_UNDEFINED) {
             return thumbnailView.getMeasuredWidth();
@@ -582,11 +573,6 @@
     @Override
     public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
             int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
-        if (enableOverviewIconMenu()) {
-            iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
-            iconParams.topMargin = 0;
-            return;
-        }
         iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
         iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2;
         iconParams.leftMargin = 0;
@@ -595,6 +581,14 @@
     }
 
     @Override
+    public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
+            int chipChildMarginStart) {
+        iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
+        iconParams.setMarginStart(chipChildMarginStart);
+        iconParams.topMargin = 0;
+    }
+
+    @Override
     public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
             FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
         boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
@@ -608,7 +602,7 @@
                 : iconMenuParams.width / 2f);
         iconAppChipView.setPivotY(
                 isRtl ? (iconMenuParams.height / 2f) : iconMenuParams.width / 2f);
-        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setSplitTranslationY(0);
         iconAppChipView.setRotation(getDegreesRotated());
     }
 
@@ -647,12 +641,14 @@
         primaryIconView.setTranslationX(0);
         secondaryIconView.setTranslationX(0);
         if (enableOverviewIconMenu()) {
+            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
             if (primaryIconView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
-                secondaryIconView.setTranslationY(-primarySnapshotHeight);
-                primaryIconView.setTranslationY(0);
+                secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
+                primaryAppChipView.setSplitTranslationY(0);
             } else {
                 int secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight;
-                primaryIconView.setTranslationY(secondarySnapshotHeight);
+                primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight);
             }
         } else if (splitConfig.initiatedFromSeascape) {
             // if the split was initiated from seascape,
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 5d9a668..60e6a25 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -21,7 +21,6 @@
 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;
 
@@ -182,12 +181,7 @@
     @Override
     public float getTaskMenuX(float x, View thumbnailView,
             DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
-        if (enableOverviewIconMenu()) {
-            if (thumbnailView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
-                return x + taskInsetMargin - taskViewIcon.getHeight() - (taskInsetMargin / 2f);
-            }
-            return x + taskInsetMargin;
-        } else if (deviceProfile.isLandscape) {
+        if (deviceProfile.isLandscape) {
             return x + taskInsetMargin
                     + (thumbnailView.getMeasuredWidth() - thumbnailView.getMeasuredHeight()) / 2f;
         } else {
@@ -198,9 +192,6 @@
     @Override
     public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
             View taskMenuView, float taskInsetMargin, View taskViewIcon) {
-        if (enableOverviewIconMenu()) {
-            return y;
-        }
         return y + taskInsetMargin;
     }
 
@@ -209,7 +200,7 @@
             @StagePosition int stagePosition) {
         if (enableOverviewIconMenu()) {
             return thumbnailView.getResources().getDimensionPixelSize(
-                    R.dimen.task_thumbnail_icon_menu_max_width);
+                    R.dimen.task_thumbnail_icon_menu_expanded_width);
         }
         int padding = thumbnailView.getResources()
                 .getDimensionPixelSize(R.dimen.task_menu_edge_padding);
@@ -623,12 +614,6 @@
     @Override
     public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
             int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
-        if (enableOverviewIconMenu()) {
-            iconParams.setMarginStart(taskIconMargin);
-            iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
-            iconParams.topMargin = 0;
-            return;
-        }
         iconParams.gravity = TOP | CENTER_HORIZONTAL;
         // Reset margins, since they may have been set on rotation
         iconParams.leftMargin = iconParams.rightMargin = 0;
@@ -636,6 +621,14 @@
     }
 
     @Override
+    public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
+            int chipChildMarginStart) {
+        iconParams.setMarginStart(chipChildMarginStart);
+        iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
+        iconParams.topMargin = 0;
+    }
+
+    @Override
     public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
             FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
         iconMenuParams.gravity = TOP | START;
@@ -646,7 +639,7 @@
 
         iconAppChipView.setPivotX(0);
         iconAppChipView.setPivotY(0);
-        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setSplitTranslationY(0);
         iconAppChipView.setRotation(getDegreesRotated());
     }
 
@@ -662,6 +655,8 @@
                 : new FrameLayout.LayoutParams(primaryIconParams);
 
         if (enableOverviewIconMenu()) {
+            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
             primaryIconParams.gravity = TOP | START;
             secondaryIconParams.gravity = TOP | START;
             secondaryIconParams.topMargin = primaryIconParams.topMargin;
@@ -669,16 +664,16 @@
             if (deviceProfile.isLeftRightSplit) {
                 if (isRtl) {
                     int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth;
-                    primaryIconView.setTranslationX(-secondarySnapshotWidth);
+                    primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth);
                 } else {
-                    secondaryIconView.setTranslationX(primarySnapshotWidth);
+                    secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth);
                 }
             } else {
-                primaryIconView.setTranslationX(0);
-                secondaryIconView.setTranslationX(0);
+                primaryAppChipView.setSplitTranslationX(0);
+                secondaryAppChipView.setSplitTranslationX(0);
                 int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
                         splitConfig.visualDividerBounds.height());
-                secondaryIconView.setTranslationY(
+                secondaryAppChipView.setSplitTranslationY(
                         primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness));
             }
         } else if (deviceProfile.isLeftRightSplit) {
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
index 9084297..01c1225 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
@@ -146,9 +146,16 @@
             int parentWidth, int parentHeight);
 
     // Overview TaskMenuView methods
+    /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
     void setTaskIconParams(FrameLayout.LayoutParams iconParams,
             int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl);
 
+    /**
+     * Sets layout params on the children of an app chip. Only use this when app chip is enabled.
+     */
+    void setIconAppChipChildrenParams(
+            FrameLayout.LayoutParams iconParams, int chipChildMarginStart);
+
     void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
             FrameLayout.LayoutParams iconMenuParams,
             int iconMenuMargin, int thumbnailTopMargin);
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
index f3001fc..a964639 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
@@ -94,9 +94,6 @@
     @Override
     public float getTaskMenuX(float x, View thumbnailView,
             DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
-        if (enableOverviewIconMenu()) {
-            return x + (taskViewIcon.getHeight() * 2);
-        }
         return x + taskInsetMargin;
     }
 
@@ -104,9 +101,7 @@
     public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
             View taskMenuView, float taskInsetMargin, View taskViewIcon) {
         if (enableOverviewIconMenu()) {
-            return y + taskViewIcon.getWidth() + (
-                    thumbnailView.getLayoutDirection() == LAYOUT_DIRECTION_RTL ? taskInsetMargin
-                            / 2f : -taskViewIcon.getHeight());
+            return y;
         }
         BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
         int taskMenuWidth = lp.width;
@@ -222,18 +217,16 @@
     @Override
     public void setTaskIconParams(FrameLayout.LayoutParams iconParams,
             int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
-        iconParams.rightMargin = 0;
-        iconParams.bottomMargin = 0;
-        if (enableOverviewIconMenu()) {
-            iconParams.setMarginStart(taskIconMargin);
-            iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
-            iconParams.leftMargin = 0;
-            iconParams.topMargin = 0;
-        } else {
-            iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
-            iconParams.leftMargin = -taskIconHeight - taskIconMargin / 2;
-            iconParams.topMargin = thumbnailTopMargin / 2;
-        }
+        iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
+        iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0);
+    }
+
+    @Override
+    public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
+            int chipChildMarginStart) {
+        iconParams.setMargins(0, 0, 0, 0);
+        iconParams.setMarginStart(chipChildMarginStart);
+        iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
     }
 
     @Override
@@ -251,7 +244,7 @@
         iconAppChipView.setPivotX(isRtl ? iconMenuParams.width / 2f : iconCenter);
         iconAppChipView.setPivotY(
                 isRtl ? iconMenuParams.width / 2f : iconCenter - iconMenuMargin);
-        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setSplitTranslationY(0);
         iconAppChipView.setRotation(getDegreesRotated());
     }
 
@@ -288,12 +281,15 @@
         primaryIconView.setTranslationX(0);
         secondaryIconView.setTranslationX(0);
         if (enableOverviewIconMenu()) {
+            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
             if (isRtl) {
-                primaryIconView.setTranslationY(groupedTaskViewHeight - primarySnapshotHeight);
-                secondaryIconView.setTranslationY(0);
+                primaryAppChipView.setSplitTranslationY(
+                        groupedTaskViewHeight - primarySnapshotHeight);
+                secondaryAppChipView.setSplitTranslationY(0);
             } else {
-                secondaryIconView.setTranslationY(-primarySnapshotHeight);
-                primaryIconView.setTranslationY(0);
+                secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
+                primaryAppChipView.setSplitTranslationY(0);
             }
         } else if (splitConfig.initiatedFromSeascape) {
             // if the split was initiated from seascape,
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index bc8b571..16f2065 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -17,6 +17,7 @@
 
 import static com.android.app.animation.Interpolators.DECELERATE;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
@@ -58,6 +59,7 @@
         FROM_APP(0.75f, 0.5f, 1f, false),
         FROM_APP_TO_ALL_APPS(1f, 0.6f, 0.8f, false),
         FROM_APP_TABLET(1f, 0.7f, 1f, true),
+        FROM_APP_TABLET_GRID_ONLY(1f, 1f, 1f, true),
         FROM_APP_TO_ALL_APPS_TABLET(1f, 0.5f, 0.5f, false),
         FROM_OVERVIEW(1f, 0.75f, 0.5f, false);
 
@@ -239,10 +241,10 @@
         float stopResist =
                 params.resistanceParams.stopScalingAtTop ? 1f - startRect.top / endRectF.top : 1f;
         final TimeInterpolator scaleInterpolator = t -> {
-            if (t < startResist) {
+            if (t <= startResist) {
                 return t;
             }
-            if (t > stopResist) {
+            if (t >= stopResist) {
                 return maxResist;
             }
             float resistProgress = Utilities.getProgress(t, startResist, stopResist);
@@ -304,7 +306,9 @@
                 resistanceParams =
                         recentsOrientedState.getActivityInterface().allowAllAppsFromOverview()
                                 ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS_TABLET
-                                : RecentsResistanceParams.FROM_APP_TABLET;
+                                : enableGridOnlyOverview()
+                                        ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY
+                                        : RecentsResistanceParams.FROM_APP_TABLET;
             } else {
                 resistanceParams =
                         recentsOrientedState.getActivityInterface().allowAllAppsFromOverview()
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 839320e..8f9395f 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -17,17 +17,22 @@
 
 package com.android.quickstep.util;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition;
 
-import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.util.Log;
+import android.util.Pair;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -39,16 +44,23 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
 
 import java.util.Arrays;
+import java.util.List;
 
 /**
  * Controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -150,7 +162,7 @@
                         task1Id = foundTask1.key.id;
                         task1Intent = null;
                     } else {
-                        task1Id = ActivityTaskManager.INVALID_TASK_ID;
+                        task1Id = INVALID_TASK_ID;
                         task1Intent = app1.intent;
                     }
 
@@ -177,6 +189,170 @@
     }
 
     /**
+     * Handles the complicated logic for how to animate an app pair entrance when already inside an
+     * app or app pair.
+     *
+     * If the user tapped on an app pair while already in an app pair, there are 4 general cases:
+     *   a) Clicked app pair A|B, but both apps are already running on screen.
+     *   b) App A is already on-screen, but App B isn't.
+     *   c) App B is on-screen, but App A isn't.
+     *   d) Neither is on-screen.
+     *
+     * If the user tapped an app pair while inside a single app, there are 3 cases:
+     *   a) The on-screen app is App A of the app pair.
+     *   b) The on-screen app is App B of the app pair.
+     *   c) It is neither.
+     *
+     * For each case, we call the appropriate animation and split launch type.
+     */
+    public void handleAppPairLaunchInApp(AppPairIcon launchingIconView,
+            List<? extends ItemInfo> itemInfos) {
+        TaskbarActivityContext context = (TaskbarActivityContext) launchingIconView.getContext();
+        List<ComponentKey> componentKeys =
+                itemInfos.stream().map(ItemInfo::getComponentKey).toList();
+
+        // Use TopTaskTracker to find the currently running app (or apps)
+        TopTaskTracker topTaskTracker = getTopTaskTracker(context);
+
+        // getRunningSplitTasksIds() will return a pair of ids if we are currently running a
+        // split pair, or an empty array with zero length if we are running a single app.
+        int[] runningSplitTasks = topTaskTracker.getRunningSplitTaskIds();
+        if (runningSplitTasks != null && runningSplitTasks.length == 2) {
+            // Tapped an app pair while in an app pair
+            int runningTaskId1 = runningSplitTasks[0];
+            int runningTaskId2 = runningSplitTasks[1];
+
+            mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+                    componentKeys,
+                    false /* findExactPairMatch */,
+                    foundTasks -> {
+                        // If our clicked app pair has already-running Tasks, we grab the
+                        // taskIds here so we can see if those ids are already on-screen now
+                        List<Integer> lastActiveTasksOfAppPair =
+                                Arrays.stream(foundTasks).map((Task task) -> {
+                                    if (task != null) {
+                                        return task.getKey().getId();
+                                    } else {
+                                        return INVALID_TASK_ID;
+                                    }
+                                }).toList();
+
+                        if (lastActiveTasksOfAppPair.contains(runningTaskId1)
+                                && lastActiveTasksOfAppPair.contains(runningTaskId2)) {
+                            // App A and App B are already on-screen, so do nothing.
+                        } else if (!lastActiveTasksOfAppPair.contains(runningTaskId1)
+                                && !lastActiveTasksOfAppPair.contains(runningTaskId2)) {
+                            // Neither A nor B are on screen, so just launch a new app pair
+                            // normally.
+                            launchAppPair(launchingIconView);
+                        } else {
+                            // Exactly one app (A or B) is on-screen, so we have to launch the other
+                            // on the appropriate side.
+                            ItemInfo app1 = itemInfos.get(0);
+                            ItemInfo app2 = itemInfos.get(1);
+                            int task1 = lastActiveTasksOfAppPair.get(0);
+                            int task2 = lastActiveTasksOfAppPair.get(1);
+
+                            // If task1 is one of the running on-screen tasks, we launch app2.
+                            // If not, task2 must be the running task, and we launch app1.
+                            ItemInfo appToLaunch =
+                                    task1 == runningTaskId1 || task1 == runningTaskId2
+                                            ? app2
+                                            : app1;
+                            // If the on-screen task is on the bottom/right position, we launch to
+                            // the top/left. If not, we launch to the bottom/right.
+                            @StagePosition int sideToLaunch =
+                                    task1 == runningTaskId2 || task2 == runningTaskId2
+                                            ? STAGE_POSITION_TOP_OR_LEFT
+                                            : STAGE_POSITION_BOTTOM_OR_RIGHT;
+
+                            launchToSide(context, launchingIconView.getInfo(), appToLaunch,
+                                    sideToLaunch);
+                        }
+                    }
+            );
+        } else {
+            // Tapped an app pair while in a single app
+            int runningTaskId = topTaskTracker
+                    .getCachedTopTask(false /* filterOnlyVisibleRecents */).getTaskId();
+
+            mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+                    componentKeys,
+                    false /* findExactPairMatch */,
+                    foundTasks -> {
+                        Task foundTask1 = foundTasks[0];
+                        Task foundTask2 = foundTasks[1];
+                        boolean task1IsOnScreen =
+                                foundTask1 != null && foundTask1.getKey().getId() == runningTaskId;
+                        boolean task2IsOnScreen =
+                                foundTask2 != null && foundTask2.getKey().getId() == runningTaskId;
+
+                        if (!task1IsOnScreen && !task2IsOnScreen) {
+                            // Neither App A nor App B are on-screen, launch the app pair normally.
+                            launchAppPair(launchingIconView);
+                        } else {
+                            // Either A or B is on-screen, so launch the other on the appropriate
+                            // side.
+                            ItemInfo app1 = itemInfos.get(0);
+                            ItemInfo app2 = itemInfos.get(1);
+                            // If task1 is the running on-screen task, we launch app2 on the
+                            // bottom/right. If task2 is on-screen, launch app1 on the top/left.
+                            ItemInfo appToLaunch = task1IsOnScreen ? app2 : app1;
+                            @StagePosition int sideToLaunch = task1IsOnScreen
+                                    ? STAGE_POSITION_BOTTOM_OR_RIGHT
+                                    : STAGE_POSITION_TOP_OR_LEFT;
+
+                            launchToSide(context, launchingIconView.getInfo(), appToLaunch,
+                                    sideToLaunch);
+                        }
+                }
+            );
+        }
+    }
+
+    /**
+     * Executes a split launch by launching an app to the side of an existing app.
+     * @param context The TaskbarActivityContext that we are launching the app pair from.
+     * @param launchingItemInfo The itemInfo of the icon that was tapped.
+     * @param app The app that will launch to the side of the existing running app (not necessarily
+     *  the same as the previous parameter; e.g. we tap an app pair but launch an app).
+     * @param side A @StagePosition, either STAGE_POSITION_TOP_OR_LEFT or
+     *  STAGE_POSITION_BOTTOM_OR_RIGHT.
+     */
+    @VisibleForTesting
+    public void launchToSide(
+            TaskbarActivityContext context,
+            ItemInfo launchingItemInfo,
+            ItemInfo app,
+            @StagePosition int side
+    ) {
+        LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+
+        // Set up to log app pair launch event
+        Pair<com.android.internal.logging.InstanceId, InstanceId> instanceIds =
+                LogUtils.getShellShareableInstanceId();
+        context.getStatsLogManager()
+                .logger()
+                .withItemInfo(launchingItemInfo)
+                .withInstanceId(instanceIds.second)
+                .log(LAUNCHER_APP_PAIR_LAUNCH);
+
+        SystemUiProxy.INSTANCE.get(context)
+                .startIntent(
+                        launcherApps.getMainActivityLaunchIntent(
+                                app.getIntent().getComponent(),
+                                null,
+                                app.user
+                        ),
+                        app.user.getIdentifier(),
+                        new Intent(),
+                        side,
+                        null,
+                        instanceIds.first
+                );
+    }
+
+    /**
      * App pair members have a "rank" attribute that contains information about the split position
      * and ratio. We implement this by splitting the int in half (e.g. 16 bits each), then use one
      * half to store splitPosition (left vs right) and the other half to store snapPosition
@@ -209,4 +385,12 @@
     public String getDefaultTitle(CharSequence app1, CharSequence app2) {
         return mContext.getString(R.string.app_pair_default_title, app1, app2);
     }
+
+    /**
+     * Gets the TopTaskTracker, which is a cached record of the top running Task.
+     */
+    @VisibleForTesting
+    public TopTaskTracker getTopTaskTracker(Context context) {
+        return TopTaskTracker.INSTANCE.get(context);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt
new file mode 100644
index 0000000..24b0e3a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.graphics.Region
+import android.os.RemoteException
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.ISystemGestureExclusionListener
+import android.view.IWindowManager
+import android.view.WindowManagerGlobal
+import androidx.annotation.BinderThread
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.util.Executors
+
+/** Wrapper over system gesture exclusion listener to optimize for multiple RPCs */
+class GestureExclusionManager(private val windowManager: IWindowManager) {
+
+    private val listeners = mutableListOf<ExclusionListener>()
+
+    private var lastExclusionRegion: Region? = null
+    private var lastUnrestrictedOrNull: Region? = null
+
+    @VisibleForTesting
+    val exclusionListener =
+        object : ISystemGestureExclusionListener.Stub() {
+            @BinderThread
+            override fun onSystemGestureExclusionChanged(
+                displayId: Int,
+                exclusionRegion: Region?,
+                unrestrictedOrNull: Region?
+            ) {
+                if (displayId != DEFAULT_DISPLAY) {
+                    return
+                }
+                Executors.MAIN_EXECUTOR.execute {
+                    lastExclusionRegion = exclusionRegion
+                    lastUnrestrictedOrNull = unrestrictedOrNull
+                    listeners.forEach {
+                        it.onGestureExclusionChanged(exclusionRegion, unrestrictedOrNull)
+                    }
+                }
+            }
+        }
+
+    /** Adds a listener for receiving gesture exclusion regions */
+    fun addListener(listener: ExclusionListener) {
+        val wasEmpty = listeners.isEmpty()
+        listeners.add(listener)
+        if (wasEmpty) {
+            Executors.UI_HELPER_EXECUTOR.execute {
+                try {
+                    windowManager.registerSystemGestureExclusionListener(
+                        exclusionListener,
+                        DEFAULT_DISPLAY
+                    )
+                } catch (e: RemoteException) {
+                    Log.e(TAG, "Failed to register gesture exclusion listener", e)
+                }
+            }
+        } else {
+            // If we had already registered before, dispatch the last known value,
+            // otherwise registering the listener will initiate a dispatch
+            listener.onGestureExclusionChanged(lastExclusionRegion, lastUnrestrictedOrNull)
+        }
+    }
+
+    /** Removes a previously added exclusion listener */
+    fun removeListener(listener: ExclusionListener) {
+        if (listeners.remove(listener) && listeners.isEmpty()) {
+            Executors.UI_HELPER_EXECUTOR.execute {
+                try {
+                    windowManager.unregisterSystemGestureExclusionListener(
+                        exclusionListener,
+                        DEFAULT_DISPLAY
+                    )
+                } catch (e: RemoteException) {
+                    Log.e(TAG, "Failed to unregister gesture exclusion listener", e)
+                }
+            }
+        }
+    }
+
+    interface ExclusionListener {
+        fun onGestureExclusionChanged(exclusionRegion: Region?, unrestrictedOrNull: Region?)
+    }
+
+    companion object {
+
+        private const val TAG = "GestureExclusionManager"
+
+        @JvmField
+        val INSTANCE = GestureExclusionManager(WindowManagerGlobal.getWindowManagerService()!!)
+
+        @JvmField val EMPTY_REGION = Region()
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
index 251b756..245dde2 100644
--- a/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
+++ b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -281,7 +281,15 @@
             if (mRectScaleAnim.canSkipToEnd()) {
                 mRectScaleAnim.skipToEnd();
             }
+            mCurrentScaleProgress = mRectScaleAnim.getSpring().getFinalPosition();
+
+            // Ensures that we end the animation with the final values.
+            mRectXAnimEnded = false;
+            mRectYAnimEnded = false;
+            mRectScaleAnimEnded = false;
+            onUpdate();
         }
+
         mRectXAnimEnded = true;
         mRectYAnimEnded = true;
         mRectScaleAnimEnded = true;
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index ad9f5ea..0ee27be 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -391,14 +391,6 @@
                 "trying to launch an app pair icon, but encountered an unexpected null"
             }
 
-            // If launching an app pair from Taskbar inside of an app context, use fade-in animation
-            // TODO (b/316485863): Replace with desired app pair launch animation
-            if (launchingIconView.context is TaskbarActivityContext) {
-                composeFadeInSplitLaunchAnimator(
-                    initialTaskId, secondTaskId, info, t, finishCallback)
-                return
-            }
-
             composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
         } else {
             // Fallback case: simple fade-in animation
@@ -483,6 +475,10 @@
      * We want to animate the Root (grandparent) so that it affects both apps and the divider.
      * To do this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the
      * left-side ones, for simplicity) and traverse the tree until we find the grandparent.
+     *
+     * This function is only called when we are animating the app pair in from scratch. It is NOT
+     * called when we are animating in from an existing visible TaskView tile or an app that is
+     * already on screen.
      */
     @VisibleForTesting
     fun composeIconSplitLaunchAnimator(
@@ -491,6 +487,14 @@
         t: Transaction,
         finishCallback: Runnable
     ) {
+        // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
+        // use the scale-up animation
+        if (launchingIconView.context is TaskbarActivityContext) {
+            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback)
+            return
+        }
+
+        // Else we are in Launcher and can launch with the full icon stretch-and-split animation.
         val launcher = Launcher.getLauncher(launchingIconView.context)
         val dp = launcher.deviceProfile
 
@@ -650,6 +654,91 @@
     }
 
     /**
+     * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when
+     * there is no visible associated tile to expand from.
+     */
+    @VisibleForTesting
+    fun composeScaleUpLaunchAnimation(
+        transitionInfo: TransitionInfo,
+        t: Transaction,
+        finishCallback: Runnable
+    ) {
+        val launchAnimation = AnimatorSet()
+        val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
+        progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION)
+        progressUpdater.interpolator = Interpolators.EMPHASIZED
+
+        var rootCandidate: Change? = null
+
+        for (change in transitionInfo.changes) {
+            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+
+            // TODO (b/316490565): Replace this logic when SplitBounds is available to
+            //  startAnimation() and we can know the precise taskIds of launching tasks.
+            // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
+            if (
+                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
+                    (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
+            ) {
+                // Found one!
+                rootCandidate = change
+                break
+            }
+        }
+
+        // If we could not find a proper root candidate, something went wrong.
+        check(rootCandidate != null) { "Could not find a split root candidate" }
+
+        // Recurse up the tree until parent is null, then we've found our root.
+        var parentToken: WindowContainerToken? = rootCandidate.parent
+        while (parentToken != null) {
+            rootCandidate = transitionInfo.getChange(parentToken) ?: break
+            parentToken = rootCandidate.parent
+        }
+
+        // Make sure nothing weird happened, like getChange() returning null.
+        check(rootCandidate != null) { "Failed to find a root leash" }
+
+        // Starting position is a 34% size tile centered in the middle of the screen.
+        // Ending position is the full device screen.
+        val screenBounds = rootCandidate.endAbsBounds
+        val startingScale = 0.34f
+        val startX =
+            screenBounds.left +
+                ((screenBounds.right - screenBounds.left) * ((1 - startingScale) / 2f))
+        val startY =
+            screenBounds.top +
+                ((screenBounds.bottom - screenBounds.top) * ((1 - startingScale) / 2f))
+        val endX = screenBounds.left
+        val endY = screenBounds.top
+
+        progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
+            val progress = valueAnimator.animatedFraction
+
+            val x = startX + ((endX - startX) * progress)
+            val y = startY + ((endY - startY) * progress)
+            val scale = startingScale + ((1 - startingScale) * progress)
+
+            t.setPosition(rootCandidate.leash, x, y)
+            t.setScale(rootCandidate.leash, scale, scale)
+            t.setAlpha(rootCandidate.leash, progress)
+            t.apply()
+        }
+
+        // When animation ends,  run finishCallback
+        progressUpdater.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    finishCallback.run()
+                }
+            }
+        )
+
+        launchAnimation.play(progressUpdater)
+        launchAnimation.start()
+    }
+
+    /**
      * If we are launching split screen without any special animation from a starting View, we
      * simply fade in the starting apps and fade out launcher.
      */
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 0bb6b23..1152de2 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -38,10 +38,12 @@
 import android.graphics.RectF;
 import android.util.Log;
 import android.view.RemoteAnimationTarget;
+import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
@@ -76,6 +78,8 @@
 
     private final Rect mTaskRect = new Rect();
     private final Rect mFullTaskSize = new Rect();
+    private final Rect mCarouselTaskSize = new Rect();
+    private PointF mPivotOverride = null;
     private final PointF mPivot = new PointF();
     private DeviceProfile mDp;
     @StagePosition
@@ -95,6 +99,11 @@
     public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
     public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
 
+    // Carousel properties
+    public final AnimatedFloat carouselScale = new AnimatedFloat();
+    public final AnimatedFloat carouselPrimaryTranslation = new AnimatedFloat();
+    public final AnimatedFloat carouselSecondaryTranslation = new AnimatedFloat();
+
     // RecentsView properties
     public final AnimatedFloat recentsViewScale = new AnimatedFloat();
     public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
@@ -109,9 +118,9 @@
     private Boolean mDrawsBelowRecents = null;
     private boolean mIsGridTask;
     private boolean mIsDesktopTask;
+    private boolean mScaleToCarouselTaskSize = false;
     private int mTaskRectTranslationX;
     private int mTaskRectTranslationY;
-    private int mPivotOffsetX;
 
     public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
         mContext = context;
@@ -124,6 +133,7 @@
         mOrientationStateId = mOrientationState.getStateId();
         Resources resources = context.getResources();
         mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
+        carouselScale.value = 1f;
     }
 
     /**
@@ -149,6 +159,11 @@
                     mOrientationState.getOrientationHandler());
         }
 
+        if (enableGridOnlyOverview()) {
+            mSizeStrategy.calculateCarouselTaskSize(mContext, mDp, mCarouselTaskSize,
+                    mOrientationState.getOrientationHandler());
+        }
+
         if (mSplitBounds != null) {
             // The task rect changes according to the staged split task sizes, but recents
             // fullscreen scale and pivot remains the same since the task fits into the existing
@@ -193,9 +208,18 @@
         }
         // Copy mFullTaskSize instead of updating it directly so it could be reused next time
         // without recalculating
-        Rect scaleRect = new Rect(mFullTaskSize);
-        scaleRect.offset(mTaskRectTranslationX + mPivotOffsetX, mTaskRectTranslationY);
-        return mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot);
+        Rect scaleRect = new Rect();
+        if (mScaleToCarouselTaskSize) {
+            scaleRect.set(mCarouselTaskSize);
+        } else {
+            scaleRect.set(mFullTaskSize);
+        }
+        scaleRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
+        float scale = mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot);
+        if (mPivotOverride != null) {
+            mPivot.set(mPivotOverride);
+        }
+        return scale;
     }
 
     /**
@@ -278,14 +302,64 @@
     /**
      * Adds animation for all the components corresponding to transition from an app to overview.
      */
-    public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
+    public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) {
         pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
-        if (enableGridOnlyOverview() && mDp.isTablet) {
-            int translationXToMiddle = mDp.widthPx / 2 - mFullTaskSize.centerX();
-            taskPrimaryTranslation.value = translationXToMiddle;
-            mPivotOffsetX = translationXToMiddle;
+        float fullScreenScale;
+        if (enableGridOnlyOverview() && mDp.isTablet && mDp.isGestureMode) {
+            // Move pivot to top right edge of the screen, to avoid task scaling down in opposite
+            // direction of app window movement, otherwise the animation will wiggle left and right.
+            // Also translate the app window to top right edge of the screen to simplify
+            // calculations.
+            taskPrimaryTranslation.value = mIsRecentsRtl
+                    ? mDp.widthPx - mFullTaskSize.right
+                    : -mFullTaskSize.left;
+            taskSecondaryTranslation.value = -mFullTaskSize.top;
+            mPivotOverride = new PointF(mIsRecentsRtl ? mDp.widthPx : 0, 0);
+
+            // Scale down to the carousel and use the carousel Rect to calculate fullScreenScale.
+            mScaleToCarouselTaskSize = true;
+            carouselScale.value = mCarouselTaskSize.width() / (float) mFullTaskSize.width();
+            fullScreenScale = getFullScreenScale();
+
+            float carouselPrimaryTranslationTarget = mIsRecentsRtl
+                    ? mCarouselTaskSize.right - mDp.widthPx
+                    : mCarouselTaskSize.left;
+            float carouselSecondaryTranslationTarget = mCarouselTaskSize.top;
+
+            // Expected carousel position's center is in the middle, and invariant of
+            // recentsViewScale.
+            float exceptedCarouselCenterX = mCarouselTaskSize.centerX();
+            // Animating carousel translations linearly will result in a curved path, therefore
+            // we'll need to calculate the expected translation at each recentsView scale. Luckily
+            // primary and secondary follow the same translation, and primary is used here due to
+            // it being simpler.
+            Interpolator carouselTranslationInterpolator = t -> {
+                // recentsViewScale is calculated rather than using recentsViewScale.value, so that
+                // this interpolator works independently even if recentsViewScale don't animate.
+                float recentsViewScale =
+                        Utilities.mapToRange(t, 0, 1, fullScreenScale, 1, Interpolators.LINEAR);
+                // Without the translation, the app window will animate from fullscreen into top
+                // right corner.
+                float expectedTaskCenterX = mIsRecentsRtl
+                        ? mDp.widthPx - mCarouselTaskSize.width() * recentsViewScale / 2f
+                        : mCarouselTaskSize.width() * recentsViewScale / 2f;
+                // Calculate the expected translation, then work back the animatedFraction that
+                // results in this value.
+                float carouselPrimaryTranslation =
+                        (exceptedCarouselCenterX - expectedTaskCenterX) / recentsViewScale;
+                return carouselPrimaryTranslation / carouselPrimaryTranslationTarget;
+            };
+
+            // Use addAnimatedFloat so this animation can later be canceled and animate to a
+            // different value in RecentsView.onPrepareGestureEndAnimation.
+            pa.addAnimatedFloat(carouselPrimaryTranslation, 0, carouselPrimaryTranslationTarget,
+                    carouselTranslationInterpolator);
+            pa.addAnimatedFloat(carouselSecondaryTranslation, 0, carouselSecondaryTranslationTarget,
+                    carouselTranslationInterpolator);
+        } else {
+            fullScreenScale = getFullScreenScale();
         }
-        pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, interpolator);
+        pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, fullScreenScale, 1, interpolator);
     }
 
     /**
@@ -382,7 +456,7 @@
 
         float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
         mCurrentFullscreenParams.setProgress(fullScreenProgress, recentsViewScale.value,
-                /* taskViewScale= */1f);
+                carouselScale.value);
 
         // Apply thumbnail matrix
         float taskWidth = mTaskRect.width();
@@ -396,6 +470,13 @@
                 taskPrimaryTranslation.value);
         mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
                 taskSecondaryTranslation.value);
+
+        mMatrix.postScale(carouselScale.value, carouselScale.value, mPivot.x, mPivot.y);
+        mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE,
+                carouselPrimaryTranslation.value);
+        mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+                carouselSecondaryTranslation.value);
+
         mOrientationState.getOrientationHandler().setPrimary(
                 mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value);
 
@@ -420,15 +501,18 @@
             return;
         }
         Log.d(TAG, "progress: " + fullScreenProgress
+                + " carouselScale: " + carouselScale.value
                 + " recentsViewScale: " + recentsViewScale.value
                 + " crop: " + mTmpCropRect
                 + " radius: " + getCurrentCornerRadius()
                 + " taskW: " + taskWidth + " H: " + taskHeight
                 + " taskRect: " + mTaskRect
                 + " taskPrimaryT: " + taskPrimaryTranslation.value
+                + " taskSecondaryT: " + taskSecondaryTranslation.value
+                + " carouselPrimaryT: " + carouselPrimaryTranslation.value
+                + " carouselSecondaryT: " + carouselSecondaryTranslation.value
                 + " recentsPrimaryT: " + recentsViewPrimaryTranslation.value
                 + " recentsSecondaryT: " + recentsViewSecondaryTranslation.value
-                + " taskSecondaryT: " + taskSecondaryTranslation.value
                 + " recentsScroll: " + recentsViewScroll.value
                 + " pivot: " + mPivot
         );
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 66c67e7..ad4b292 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -398,8 +398,11 @@
                             layoutParams.width,
                             layoutParams.height
                     );
-            int iconMargins = getResources().getDimensionPixelSize(
-                    R.dimen.task_thumbnail_icon_menu_start_margin) * 2;
+            int iconViewMarginStart = getResources().getDimensionPixelSize(
+                    R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
+            int iconViewBackgroundMarginStart = getResources().getDimensionPixelSize(
+                    R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
+            int iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2;
             ((IconAppChipView) mIconView).setMaxWidth(groupedTaskViewSizes.first.x - iconMargins);
             ((IconAppChipView) mIconView2).setMaxWidth(groupedTaskViewSizes.second.x - iconMargins);
         }
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
index 3347665..cb3566e 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -17,30 +17,35 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
 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.view.ViewOutlineProvider;
 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.Utilities;
+import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.RecentsOrientedState;
 
@@ -59,38 +64,93 @@
 
     private final MultiValueAlpha mMultiValueAlpha;
 
+    private View mMenuAnchorView;
     private IconView mIconView;
     // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
     private TextView mIconTextCollapsedView;
     private TextView mIconTextExpandedView;
     private ImageView mIconArrowView;
-    private ImageView mIconViewBackground;
-    // Use separate views for the rounded corners so we can scale the background view without
-    // warping the corners.
-    private ImageView mIconViewBackgroundCornersStart;
-    private ImageView mIconViewBackgroundCornersEnd;
-
-    private final int mMinimumMenuSize;
-    private final int mMaxMenuWidth;
-    private final int mIconMenuMarginTop;
-    private final int mIconMenuMarginStart;
+    private final Rect mBackgroundRelativeLtrLocation = new Rect();
+    final RectEvaluator mBackgroundAnimationRectEvaluator =
+            new RectEvaluator(mBackgroundRelativeLtrLocation);
+    private final int mCollapsedMenuDefaultWidth;
+    private final int mExpandedMenuDefaultWidth;
+    private final int mCollapsedMenuDefaultHeight;
+    private final int mExpandedMenuDefaultHeight;
+    private final int mIconMenuMarginTopStart;
+    private final int mMenuToChipGap;
+    private final int mBackgroundMarginTopStart;
+    private final int mAppNameHorizontalMargin;
     private final int mIconViewMarginStart;
-    private final int mIconViewDrawableSize;
-    private final int mIconViewDrawableMaxSize;
-    private final int mIconTextMinWidth;
-    private final int mIconTextMaxWidth;
-    private final int mTextMaxTranslationX;
-    private final int mInnerMargin;
-    private final float mArrowMaxTranslationX;
-    private final int mMinIconBackgroundWidth;
-    private final int mMaxIconBackgroundHeight;
-    private final int mMinIconBackgroundHeight;
-    private final int mMaxIconBackgroundCornerRadius;
-    private final float mMinIconBackgroundCornerRadius;
+    private final int mAppIconSize;
+    private final int mArrowSize;
+    private final int mIconViewDrawableExpandedSize;
+    private final int mArrowMarginEnd;
     private AnimatorSet mAnimator;
 
     private int mMaxWidth = Integer.MAX_VALUE;
 
+    private static final int INDEX_SPLIT_TRANSLATION = 0;
+    private static final int INDEX_MENU_TRANSLATION = 1;
+    private static final int INDEX_COUNT_TRANSLATION = 2;
+
+    private final MultiPropertyFactory<View> mViewTranslationX;
+    private final MultiPropertyFactory<View> mViewTranslationY;
+
+    /**
+     * Sets the view split x-axis translation
+     * @param translationX x-axis translation
+     */
+    public void setSplitTranslationX(float translationX) {
+        mViewTranslationX.get(INDEX_SPLIT_TRANSLATION).setValue(translationX);
+    }
+
+    /**
+     * Sets the view split y-axis translation
+     * @param translationY y-axis translation
+     */
+    public void setSplitTranslationY(float translationY) {
+        mViewTranslationY.get(INDEX_SPLIT_TRANSLATION).setValue(translationY);
+    }
+
+    /**
+     * Gets the menu x-axis translation for split task
+     */
+    public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() {
+        return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
+    }
+
+    /**
+     * Translate the View on the X-axis without overriding the raw translation.
+     * This function is used for the menu split animation. It allows external animations to
+     * translate this view without affecting the value of the original translation. Thus,
+     * it is possible to restore the initial translation value.
+     *
+     * @param translationX Animated translation to be aggregated to the raw translation.
+     */
+    public void setMenuTranslationX(float translationX) {
+        mViewTranslationX.get(INDEX_MENU_TRANSLATION).setValue(translationX);
+    }
+
+    /**
+     * Gets the menu y-axis translation for split task
+     */
+    public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
+        return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
+    }
+
+    /**
+     * Translate the View on the Y-axis without overriding the raw translation.
+     * This function is used for the menu split animation. It allows external animations to
+     * translate this view without affecting the value of the original translation. Thus,
+     * it is possible to restore the initial translation value.
+     *
+     * @param translationY Animated translation to be aggregated to the raw translation.
+     */
+    public void setMenuTranslationY(float translationY) {
+        mViewTranslationY.get(INDEX_MENU_TRANSLATION).setValue(translationY);
+    }
+
     public IconAppChipView(Context context) {
         this(context, null);
     }
@@ -111,51 +171,42 @@
         mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
 
         // Menu dimensions
-        mMaxMenuWidth = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_max_width);
-        mIconMenuMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_top_margin);
-        mIconMenuMarginStart = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_start_margin);
+        mCollapsedMenuDefaultWidth =
+                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width);
+        mExpandedMenuDefaultWidth =
+                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width);
+        mCollapsedMenuDefaultHeight =
+                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height);
+        mExpandedMenuDefaultHeight =
+                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height);
+        mIconMenuMarginTopStart = res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
+        mMenuToChipGap = res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_expanded_gap);
 
         // Background dimensions
-        mMinIconBackgroundWidth = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_background_min_width);
-        mMaxIconBackgroundHeight = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_max_height);
-        mMinIconBackgroundHeight = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_min_height);
-        mMaxIconBackgroundCornerRadius = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_corner_radius);
+        mBackgroundMarginTopStart = res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
 
-        // TextView dimensions
-        mInnerMargin = (int) res.getDimension(R.dimen.task_thumbnail_icon_menu_arrow_margin);
-        mIconTextMinWidth = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_text_width);
-        mIconTextMaxWidth = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_text_max_width);
-
-        // IconView dimensions
+        // Contents dimensions
+        mAppNameHorizontalMargin = res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed);
+        mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin);
         mIconViewMarginStart = res.getDimensionPixelSize(
                 R.dimen.task_thumbnail_icon_view_start_margin);
-        mIconViewDrawableSize = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_drawable_size);
-        mIconViewDrawableMaxSize = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_drawable_max_size);
-        mTextMaxTranslationX =
-                (mIconViewDrawableMaxSize - mIconViewDrawableSize - mIconViewMarginStart)
-                        + (mInnerMargin / 2);
-
-        // ArrowView dimensions
-        int iconArrowViewWidth = res.getDimensionPixelSize(
+        mAppIconSize = res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size);
+        mArrowSize = res.getDimensionPixelSize(
                 R.dimen.task_thumbnail_icon_menu_arrow_size);
-        mMinIconBackgroundCornerRadius = mMinIconBackgroundHeight / 2f;
-        float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
-                mMaxIconBackgroundCornerRadius);
-        mArrowMaxTranslationX = (mMaxMenuWidth - maxCornerSize) - (Math.min(mMaxWidth,
-                mMinIconBackgroundWidth + (2 * mMinIconBackgroundCornerRadius)
-                        - mMinIconBackgroundCornerRadius)) - mInnerMargin;
+        mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
+                R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
 
-        // Menu dimensions
-        mMinimumMenuSize =
-                mIconViewMarginStart + mIconViewDrawableSize + mInnerMargin + iconArrowViewWidth;
+        mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
+                INDEX_COUNT_TRANSLATION,
+                Float::sum);
+        mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
+                INDEX_COUNT_TRANSLATION,
+                Float::sum);
     }
 
     @Override
@@ -165,9 +216,7 @@
         mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed);
         mIconTextExpandedView = findViewById(R.id.icon_text_expanded);
         mIconArrowView = findViewById(R.id.icon_arrow);
-        mIconViewBackground = findViewById(R.id.icon_view_background);
-        mIconViewBackgroundCornersStart = findViewById(R.id.icon_view_background_corners_start);
-        mIconViewBackgroundCornersEnd = findViewById(R.id.icon_view_background_corners_end);
+        mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor);
     }
 
     protected IconView getIconView() {
@@ -204,88 +253,85 @@
     }
 
     /**
-     * Sets the maximum width of this Icon Menu.
+     * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
+     * split screen.
      */
     public void setMaxWidth(int maxWidth) {
-        // Only the app icon and caret are visible at its minimum width.
-        mMaxWidth = Math.max(maxWidth, mMinimumMenuSize);
+        // Width showing only the app icon and arrow. Max width should not be set to less than this.
+        int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd;
+        mMaxWidth = Math.max(maxWidth, minimumMaxWidth);
     }
 
     @Override
     public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
         RecentsPagedOrientationHandler orientationHandler =
                 orientationState.getOrientationHandler();
-        boolean isRtl = isLayoutRtl();
-        DeviceProfile deviceProfile =
-                ActivityContext.lookupContext(getContext()).getDeviceProfile();
+        // Layout params for anchor view
+        LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams();
+        anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap;
+        mMenuAnchorView.setLayoutParams(anchorLayoutParams);
 
-        // Layout Params for the Menu View
-        int thumbnailTopMargin =
-                deviceProfile.overviewTaskThumbnailTopMarginPx + mIconMenuMarginTop;
+        // Layout Params for the Menu View (this)
         LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
-        orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginStart,
-                thumbnailTopMargin);
-        iconMenuParams.width = Math.min(mMaxWidth,
-                mMinIconBackgroundWidth + (int) (2 * mMinIconBackgroundCornerRadius));
-        iconMenuParams.height = mMinIconBackgroundHeight;
+        iconMenuParams.width = mExpandedMenuDefaultWidth;
+        iconMenuParams.height = mExpandedMenuDefaultHeight;
+        orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart,
+                mIconMenuMarginTopStart);
         setLayoutParams(iconMenuParams);
 
+        // Layout params for the background
+        Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
+        mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds);
+        setOutlineProvider(new ViewOutlineProvider() {
+            final Rect mRtlAppliedOutlineBounds = new Rect();
+            @Override
+            public void getOutline(View view, Outline outline) {
+                mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation);
+                if (isLayoutRtl()) {
+                    int width = getWidth();
+                    mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right;
+                    mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left;
+                }
+                outline.setRoundRect(
+                        mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f);
+            }
+        });
+
         // Layout Params for the Icon View
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
-        orientationHandler.setTaskIconParams(iconParams, mIconViewMarginStart,
-                mIconViewDrawableSize, thumbnailTopMargin, isRtl);
-        iconParams.width = iconParams.height = mIconViewDrawableSize;
+        int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart;
+        orientationHandler.setIconAppChipChildrenParams(
+                iconParams, iconMarginStartRelativeToParent);
+
         mIconView.setLayoutParams(iconParams);
-        mIconView.setDrawableSize(mIconViewDrawableSize, mIconViewDrawableSize);
+        mIconView.setDrawableSize(mAppIconSize, mAppIconSize);
 
         // Layout Params for the collapsed Icon Text View
+        int textMarginStart =
+                iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin;
         LayoutParams iconTextCollapsedParams =
                 (LayoutParams) mIconTextCollapsedView.getLayoutParams();
-        orientationHandler.setTaskIconParams(iconTextCollapsedParams, 0, mIconViewDrawableSize,
-                thumbnailTopMargin, isRtl);
-        iconTextCollapsedParams.setMarginStart(
-                mIconViewDrawableSize + mIconViewMarginStart + mInnerMargin);
-        iconTextCollapsedParams.topMargin = (mMinIconBackgroundHeight - mIconViewDrawableSize) / 2;
-        iconTextCollapsedParams.gravity = Gravity.TOP | Gravity.START;
-        iconTextCollapsedParams.width = Math.min(
-                Math.max(mMaxWidth - mMinimumMenuSize - (2 * mInnerMargin), 0), mIconTextMinWidth);
+        orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart);
+        int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart
+                - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd;
+        iconTextCollapsedParams.width = collapsedTextWidth;
         mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams);
         mIconTextCollapsedView.setAlpha(1f);
 
         // Layout Params for the expanded Icon Text View
         LayoutParams iconTextExpandedParams =
                 (LayoutParams) mIconTextExpandedView.getLayoutParams();
-        orientationHandler.setTaskIconParams(iconTextExpandedParams, 0, mIconViewDrawableSize,
-                thumbnailTopMargin, isRtl);
-        iconTextExpandedParams.setMarginStart(
-                mIconViewDrawableSize + mIconViewMarginStart + mInnerMargin);
-        iconTextExpandedParams.topMargin = (mMinIconBackgroundHeight - mIconViewDrawableSize) / 2;
-        iconTextExpandedParams.gravity = Gravity.TOP | Gravity.START;
+        orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart);
         mIconTextExpandedView.setLayoutParams(iconTextExpandedParams);
         mIconTextExpandedView.setAlpha(0f);
-        mIconTextExpandedView.setRevealClip(true, 0, mIconViewDrawableSize / 2f, mIconTextMinWidth);
+        mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth);
 
         // Layout Params for the Icon Arrow View
         LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
-        iconArrowParams.gravity = Gravity.CENTER_VERTICAL | Gravity.END;
-        iconArrowParams.setMarginEnd(mInnerMargin);
+        int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
+        orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
         mIconArrowView.setLayoutParams(iconArrowParams);
 
-        // Layout Params for the Icon View Background and its corners
-        int cornerlessBackgroundWidth = (int) Math.min(
-                mMaxWidth - (2 * mMinIconBackgroundCornerRadius), mMinIconBackgroundWidth);
-        LayoutParams backgroundCornerEndParams =
-                (LayoutParams) mIconViewBackgroundCornersEnd.getLayoutParams();
-        backgroundCornerEndParams.setMarginStart(cornerlessBackgroundWidth);
-        mIconViewBackgroundCornersEnd.setLayoutParams(backgroundCornerEndParams);
-        LayoutParams backgroundParams = (LayoutParams) mIconViewBackground.getLayoutParams();
-        backgroundParams.width = cornerlessBackgroundWidth;
-        backgroundParams.height = mMinIconBackgroundHeight;
-        backgroundParams.setMarginStart((int) mMinIconBackgroundCornerRadius);
-        mIconViewBackground.setLayoutParams(backgroundParams);
-        mIconViewBackground.setPivotX(isRtl ? cornerlessBackgroundWidth : 0);
-        mIconViewBackground.setPivotY(mMinIconBackgroundCornerRadius);
-
         // This method is called twice sometimes (like when rotating split tasks). It is called
         // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
         // a new width. This happens because we update widths on rotation and on measure of
@@ -324,108 +370,96 @@
 
     protected void revealAnim(boolean isRevealing) {
         cancelInProgressAnimations();
+        final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
+        final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds();
+        final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation);
+        mAnimator = new AnimatorSet();
 
         if (isRevealing) {
             boolean isRtl = isLayoutRtl();
             bringToFront();
             ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).start();
-            mAnimator = new AnimatorSet();
-            float backgroundScaleY = mMaxIconBackgroundHeight / (float) mMinIconBackgroundHeight;
-            float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
-                    mMaxIconBackgroundCornerRadius);
-            float backgroundScaleX = (mMaxMenuWidth - (2 * maxCornerSize)) / Math.min(
-                    mMaxWidth - (2 * mMinIconBackgroundCornerRadius), mMinIconBackgroundWidth);
-            float arrowTranslationX = mArrowMaxTranslationX + (mMinIconBackgroundWidth - Math.min(
-                    mMaxWidth - (2 * mMinIconBackgroundCornerRadius), mMinIconBackgroundWidth));
             // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
             Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
-                    mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, 0,
-                    mIconTextMaxWidth + maxCornerSize);
-            expandedTextRevealAnim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    // createCircularReveal removes clip on finish, restore it here to clip text.
-                    mIconTextExpandedView.setRevealClip(true, 0,
-                            mIconTextExpandedView.getHeight() / 2f,
-                            mIconTextMaxWidth + maxCornerSize);
-                }
-            });
+                    mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
+                    mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth());
+            // Animate background clipping
+            ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
+                    mBackgroundAnimationRectEvaluator,
+                    initialBackground,
+                    expandedBackgroundBounds);
+            backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
+
+            float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize;
+            float arrowTranslationX =
+                    expandedBackgroundBounds.right - collapsedBackgroundBounds.right;
+            float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin;
+            float iconCenterToTextExpanded =
+                    mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin;
+            float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed;
+
+            float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX;
+            float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX;
+
             mAnimator.playTogether(
                     expandedTextRevealAnim,
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y,
-                            backgroundScaleY),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_X,
-                            backgroundScaleY),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_Y,
-                            backgroundScaleY),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_X,
-                            backgroundScaleY),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, TRANSLATION_X,
-                            isRtl ? -arrowTranslationX : arrowTranslationX),
-                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_X, backgroundScaleX),
-                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_Y, backgroundScaleY),
-                    ObjectAnimator.ofFloat(mIconView, SCALE_X,
-                            mIconViewDrawableMaxSize / (float) mIconViewDrawableSize),
-                    ObjectAnimator.ofFloat(mIconView, SCALE_Y,
-                            mIconViewDrawableMaxSize / (float) mIconViewDrawableSize),
+                    backgroundAnimator,
+                    ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling),
+                    ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling),
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X,
-                            isLayoutRtl() ? -mTextMaxTranslationX : mTextMaxTranslationX),
+                            textTranslationXWithRtl),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X,
-                            isLayoutRtl() ? -mTextMaxTranslationX : mTextMaxTranslationX),
+                            textTranslationXWithRtl),
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
-                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X,
-                            isRtl ? -arrowTranslationX : arrowTranslationX));
+                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl));
             mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
-            mAnimator.setInterpolator(EMPHASIZED);
-            mAnimator.start();
         } else {
             ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reverse();
-            float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
-                    mMaxIconBackgroundCornerRadius);
             // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
             Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
                     mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
-                    mIconTextMaxWidth + maxCornerSize, 0);
-            expandedTextClipAnim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    // createCircularReveal removes clip on finish, restore it here to clip text.
-                    mIconTextExpandedView.setRevealClip(true, 0,
-                            mIconTextExpandedView.getHeight() / 2f, 0);
-                }
-            });
-            mAnimator = new AnimatorSet();
+                    mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth());
+
+            // Animate background clipping
+            ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
+                    mBackgroundAnimationRectEvaluator,
+                    initialBackground,
+                    collapsedBackgroundBounds);
+            backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
+
             mAnimator.playTogether(
                     expandedTextClipAnim,
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_X, 1),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y, 1),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_X, 1),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_Y, 1),
-                    ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, TRANSLATION_X, 0),
-                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_X, 1),
-                    ObjectAnimator.ofFloat(mIconViewBackground, SCALE_Y, 1),
-                    ObjectAnimator.ofFloat(mIconView, SCALE_X, 1),
-                    ObjectAnimator.ofFloat(mIconView, SCALE_Y, 1),
+                    backgroundAnimator,
+                    ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1),
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
                     ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0));
             mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
-            mAnimator.setInterpolator(EMPHASIZED);
-            mAnimator.start();
         }
+
+        mAnimator.setInterpolator(EMPHASIZED);
+        mAnimator.start();
+    }
+
+    private Rect getCollapsedBackgroundLtrBounds() {
+        Rect bounds = new Rect(
+                0,
+                0,
+                Math.min(mMaxWidth, mCollapsedMenuDefaultWidth),
+                mCollapsedMenuDefaultHeight);
+        bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart);
+        return bounds;
+    }
+
+    private Rect getExpandedBackgroundLtrBounds() {
+        return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
     }
 
     protected void reset() {
-        mIconViewBackgroundCornersStart.setScaleX(1);
-        mIconViewBackgroundCornersStart.setScaleY(1);
-        mIconViewBackgroundCornersEnd.setScaleX(1);
-        mIconViewBackgroundCornersEnd.setScaleY(1);
-        mIconViewBackgroundCornersEnd.setTranslationX(0);
-        mIconViewBackground.setScaleX(1);
-        mIconViewBackground.setScaleY(1);
+        mBackgroundRelativeLtrLocation.set(getCollapsedBackgroundLtrBounds());
         mIconView.setScaleX(1);
         mIconView.setScaleY(1);
         mIconTextCollapsedView.setTranslationX(0);
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
index 4d33fda..1312ec3 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep.views;
 
-import static com.android.launcher3.Flags.enableOverviewIconMenu;
-
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
@@ -196,10 +194,8 @@
         setLayoutParams(iconParams);
 
         setRotation(orientationHandler.getDegreesRotated());
-        int iconDrawableSize = enableOverviewIconMenu()
-                ? deviceProfile.overviewTaskIconAppChipMenuDrawableSizePx
-                : isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
-                        : deviceProfile.overviewTaskIconDrawableSizePx;
+        int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
+                : deviceProfile.overviewTaskIconDrawableSizePx;
         setDrawableSize(iconDrawableSize, iconDrawableSize);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9884d8d..f6afaf0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -459,6 +459,7 @@
 
     @Nullable
     protected RemoteTargetHandle[] mRemoteTargetHandles;
+    protected final Rect mLastComputedCarouselTaskSize = new Rect();
     protected final Rect mLastComputedTaskSize = new Rect();
     protected final Rect mLastComputedGridSize = new Rect();
     protected final Rect mLastComputedGridTaskSize = new Rect();
@@ -2108,6 +2109,10 @@
             mSizeStrategy.calculateDesktopTaskSize(mActivity, mActivity.getDeviceProfile(),
                     mLastComputedDesktopTaskSize);
         }
+        if (enableGridOnlyOverview()) {
+            mSizeStrategy.calculateCarouselTaskSize(mActivity, dp, mLastComputedCarouselTaskSize,
+                    getPagedOrientationHandler());
+        }
 
         mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
         mTopBottomRowHeightDiff =
@@ -2137,9 +2142,12 @@
         }
 
         float accumulatedTranslationX = 0;
-        float translateXToMiddle = enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet
-                ? mActivity.getDeviceProfile().widthPx / 2 - mLastComputedGridTaskSize.centerX()
-                : 0;
+        float translateXToMiddle = 0;
+        if (enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet) {
+            translateXToMiddle = mIsRtl
+                    ? mLastComputedCarouselTaskSize.right - mLastComputedTaskSize.right
+                    : mLastComputedCarouselTaskSize.left - mLastComputedTaskSize.left;
+        }
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = requireTaskViewAt(i);
             taskView.updateTaskSize();
@@ -2206,6 +2214,10 @@
         return mLastComputedDesktopTaskSize;
     }
 
+    public Rect getLastComputedCarouselTaskSize() {
+        return mLastComputedCarouselTaskSize;
+    }
+
     /** Gets the task size for modal state. */
     public void getModalTaskSize(Rect outRect) {
         mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
@@ -2675,6 +2687,9 @@
                     tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
                 } else {
                     animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
+                    animatorSet.play(tvs.carouselScale.animateToValue(1));
+                    animatorSet.play(tvs.carouselPrimaryTranslation.animateToValue(0));
+                    animatorSet.play(tvs.carouselSecondaryTranslation.animateToValue(0));
                     animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
                             runningTaskPrimaryGridTranslation));
                     animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
@@ -4411,12 +4426,13 @@
                 mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
             }
         } else {
-            mTempRect.set(mLastComputedTaskSize);
             // Only update pivot when it is tablet and not in grid yet, so the pivot is correct
             // for non-current tasks when swiping up to overview
             if (enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet
                     && !mOverviewGridEnabled) {
-                mTempRect.offset(mActivity.getDeviceProfile().widthPx / 2 - mTempRect.centerX(), 0);
+                mTempRect.set(mLastComputedCarouselTaskSize);
+            } else {
+                mTempRect.set(mLastComputedTaskSize);
             }
             getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect,
                     mActivity.getDeviceProfile(), mTempPointF);
@@ -5117,7 +5133,12 @@
      * Returns the scale up required on the view, so that it coves the screen completely
      */
     public float getMaxScaleForFullScreen() {
-        getTaskSize(mTempRect);
+        if (enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet
+                && !mOverviewGridEnabled) {
+            mTempRect.set(mLastComputedCarouselTaskSize);
+        } else {
+            mTempRect.set(mLastComputedTaskSize);
+        }
         return getPagedViewOrientedState().getFullScreenScaleAndPivot(
                 mTempRect, mActivity.getDeviceProfile(), mTempPointF);
     }
@@ -5545,7 +5566,7 @@
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = requireTaskViewAt(i);
             float scrollDiff = taskView.getScrollAdjustment(showAsGrid);
-            int pageScroll = newPageScrolls[i] + (int) scrollDiff;
+            int pageScroll = newPageScrolls[i] + Math.round(scrollDiff);
             if ((mIsRtl && pageScroll < lastTaskScroll)
                     || (!mIsRtl && pageScroll > lastTaskScroll)) {
                 pageScroll = lastTaskScroll;
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index cf50835..39f1e46 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -18,6 +18,7 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.Flags.enableOverviewIconMenu;
+import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
@@ -78,8 +79,6 @@
     private LinearLayout mOptionLayout;
     private float mMenuTranslationYBeforeOpen;
     private float mMenuTranslationXBeforeOpen;
-    private float mIconViewTranslationYBeforeOpen;
-    private float mIconViewTranslationXBeforeOpen;
 
     public TaskMenuView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -225,8 +224,10 @@
         // Get Position
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         mActivity.getDragLayer().getDescendantRectRelativeToSelf(
-                enableOverviewIconMenu() ? taskContainer.getIconView().asView()
-                        : taskContainer.getThumbnailView(), sTempRect);
+                enableOverviewIconMenu()
+                        ? getIconView().findViewById(R.id.icon_view_menu_anchor)
+                        : taskContainer.getThumbnailView(),
+                sTempRect);
         Rect insets = mActivity.getDragLayer().getInsets();
         BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
         params.width = orientationHandler.getTaskMenuWidth(taskContainer.getThumbnailView(),
@@ -246,15 +247,9 @@
 
         orientationHandler.setTaskOptionsMenuLayoutOrientation(
                 deviceProfile, mOptionLayout, dividerSpacing, divider);
-        float thumbnailAlignedX = sTempRect.left - insets.left + (enableOverviewIconMenu()
-                ? -getResources().getDimensionPixelSize(
-                        R.dimen.task_thumbnail_icon_menu_touch_max_margin)
-                        - getResources().getDimension(R.dimen.task_thumbnail_icon_menu_start_margin)
-                : 0);
-        float thumbnailAlignedY = sTempRect.top - insets.top + (enableOverviewIconMenu()
-                ? getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_max_height)
-                        - getResources().getDimensionPixelSize(
-                        R.dimen.task_thumbnail_icon_menu_top_margin) : 0);
+        float thumbnailAlignedX = sTempRect.left - insets.left;
+        float thumbnailAlignedY = sTempRect.top - insets.top;
+
         // 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
@@ -262,31 +257,33 @@
         setPivotY(0);
         setRotation(orientationHandler.getDegreesRotated());
 
-        // Margin that insets the menuView inside the taskView
-        float taskInsetMarginX = enableOverviewIconMenu() ? getResources().getDimension(
-                R.dimen.task_thumbnail_icon_menu_start_margin) : getResources().getDimension(
-                R.dimen.task_card_margin);
-        float taskInsetMarginY = enableOverviewIconMenu() ? getResources().getDimension(
-                R.dimen.task_thumbnail_icon_menu_start_margin) : getResources().getDimension(
-                R.dimen.task_card_margin);
-        setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
-                mTaskContainer.getThumbnailView(), deviceProfile, taskInsetMarginX,
-                mTaskContainer.getIconView().asView()));
-        setTranslationY(orientationHandler.getTaskMenuY(
-                thumbnailAlignedY, mTaskContainer.getThumbnailView(),
-                mTaskContainer.getStagePosition(), this, taskInsetMarginY,
-                mTaskContainer.getIconView().asView()));
+        if (enableOverviewIconMenu()) {
+            setTranslationX(thumbnailAlignedX);
+            setTranslationY(thumbnailAlignedY);
+        } else {
+            // Margin that insets the menuView inside the taskView
+            float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
+            setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
+                    mTaskContainer.getThumbnailView(), deviceProfile, taskInsetMargin,
+                    getIconView()));
+            setTranslationY(orientationHandler.getTaskMenuY(
+                    thumbnailAlignedY, mTaskContainer.getThumbnailView(),
+                    mTaskContainer.getStagePosition(), this, taskInsetMargin,
+                    getIconView()));
+        }
     }
 
     private void animateOpen() {
         mMenuTranslationYBeforeOpen = getTranslationY();
         mMenuTranslationXBeforeOpen = getTranslationX();
-        mIconViewTranslationYBeforeOpen = mTaskContainer.getIconView().asView().getTranslationY();
-        mIconViewTranslationXBeforeOpen = mTaskContainer.getIconView().asView().getTranslationX();
         animateOpenOrClosed(false);
         mIsOpen = true;
     }
 
+    private View getIconView() {
+        return mTaskContainer.getIconView().asView();
+    }
+
     private void animateClose() {
         animateOpenOrClosed(true);
     }
@@ -318,19 +315,15 @@
                 float midpoint = (taskBottom + taskbarTop) / 2f;
                 additionalTranslationY = -Math.max(menuBottom - midpoint, 0);
             }
-            // Translate the menu to account for the expansion of the app chip menu as well.
-            float expandOffsetTranslationY = getResources().getDimensionPixelSize(
-                    R.dimen.task_thumbnail_icon_menu_expanded_gap);
             ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(this, TRANSLATION_Y,
                     closing ? mMenuTranslationYBeforeOpen
-                            : mMenuTranslationYBeforeOpen + additionalTranslationY
-                                    + expandOffsetTranslationY);
+                            : mMenuTranslationYBeforeOpen + additionalTranslationY);
             translationYAnim.setInterpolator(EMPHASIZED);
 
+            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
             ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
-                    mTaskContainer.getIconView().asView(), TRANSLATION_Y,
-                    closing ? mIconViewTranslationYBeforeOpen
-                            : mIconViewTranslationYBeforeOpen + additionalTranslationY);
+                    iconAppChip.getMenuTranslationY(),
+                    MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY);
             menuTranslationYAnim.setInterpolator(EMPHASIZED);
 
             mOpenCloseAnimator.playTogether(translationYAnim, menuTranslationYAnim);
@@ -348,10 +341,10 @@
                             : mMenuTranslationXBeforeOpen - additionalTranslationX);
             translationXAnim.setInterpolator(EMPHASIZED);
 
+            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
             ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat(
-                    mTaskContainer.getIconView().asView(), TRANSLATION_X,
-                    closing ? mIconViewTranslationXBeforeOpen
-                            : mIconViewTranslationXBeforeOpen - additionalTranslationX);
+                    iconAppChip.getMenuTranslationX(),
+                    MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX);
             menuTranslationXAnim.setInterpolator(EMPHASIZED);
 
             mOpenCloseAnimator.playTogether(translationXAnim, menuTranslationXAnim);
@@ -391,9 +384,11 @@
 
     private void resetOverviewIconMenu() {
         if (enableOverviewIconMenu()) {
-            ((IconAppChipView) mTaskContainer.getIconView()).reset();
+            IconAppChipView iconAppChipView = (IconAppChipView) mTaskContainer.getIconView();
+            iconAppChipView.reset();
+            iconAppChipView.setMenuTranslationX(0);
+            iconAppChipView.setMenuTranslationY(0);
             setTranslationY(mMenuTranslationYBeforeOpen);
-            mTaskContainer.getIconView().asView().setTranslationY(mIconViewTranslationYBeforeOpen);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 5057c38..55da160 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -23,6 +23,7 @@
 import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.Flags.enableOverviewIconMenu;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
@@ -1750,7 +1751,13 @@
             expectedHeight = boxHeight + thumbnailPadding;
 
             // Scale to to fit task Rect.
-            nonGridScale = taskWidth / (float) boxWidth;
+            if (enableGridOnlyOverview()) {
+                final Rect lastComputedCarouselTaskSize =
+                        getRecentsView().getLastComputedCarouselTaskSize();
+                nonGridScale = lastComputedCarouselTaskSize.width() / (float) taskWidth;
+            } else {
+                nonGridScale = taskWidth / (float) boxWidth;
+            }
 
             // Align to top of task Rect.
             boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f;
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index 53bc2a2..2916952 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -2,17 +2,15 @@
 
 import android.content.Context
 import android.testing.AndroidTestingRunner
-import android.view.Display
-import android.view.IWindowManager
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.SmallTest
 import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
 import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE
 import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
 import com.android.launcher3.util.DisplayController.Info
-import com.android.launcher3.util.Executors
 import com.android.launcher3.util.NavigationMode
 import com.android.launcher3.util.window.WindowManagerProxy
+import com.android.quickstep.util.GestureExclusionManager
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,7 +26,7 @@
 @RunWith(AndroidTestingRunner::class)
 class RecentsAnimationDeviceStateTest {
 
-    @Mock private lateinit var windowManager: IWindowManager
+    @Mock private lateinit var exclusionManager: GestureExclusionManager
     @Mock private lateinit var windowManagerProxy: WindowManagerProxy
     @Mock private lateinit var info: Info
 
@@ -38,60 +36,45 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest = RecentsAnimationDeviceState(context, windowManager)
+        underTest = RecentsAnimationDeviceState(context, exclusionManager)
     }
 
     @Test
     fun registerExclusionListener_success() {
         underTest.registerExclusionListener()
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .registerSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).addListener(underTest)
     }
 
     @Test
     fun registerExclusionListener_again_fail() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.registerExclusionListener()
 
-        awaitTasksCompleted()
-        verifyZeroInteractions(windowManager)
+        verifyZeroInteractions(exclusionManager)
     }
 
     @Test
     fun unregisterExclusionListener_success() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.unregisterExclusionListener()
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .unregisterSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).removeListener(underTest)
     }
 
     @Test
     fun unregisterExclusionListener_again_fail() {
         underTest.registerExclusionListener()
         underTest.unregisterExclusionListener()
-        awaitTasksCompleted()
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.unregisterExclusionListener()
 
-        awaitTasksCompleted()
-        verifyZeroInteractions(windowManager)
+        verifyZeroInteractions(exclusionManager)
     }
 
     @Test
@@ -100,45 +83,28 @@
 
         underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .registerSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).addListener(underTest)
     }
 
     @Test
     fun onDisplayInfoChanged_twoButton_unregisterExclusionListener() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
         whenever(info.getNavigationMode()).thenReturn(NavigationMode.TWO_BUTTONS)
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .unregisterSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).removeListener(underTest)
     }
 
     @Test
     fun onDisplayInfoChanged_changeDensity_noOp() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
         whenever(info.getNavigationMode()).thenReturn(NavigationMode.NO_BUTTON)
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.onDisplayInfoChanged(context, info, CHANGE_DENSITY)
 
-        awaitTasksCompleted()
-        verifyZeroInteractions(windowManager)
-    }
-
-    private fun awaitTasksCompleted() {
-        Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+        verifyZeroInteractions(exclusionManager)
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index 1723844..510faf6 100644
--- a/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt
@@ -18,18 +18,37 @@
 
 import android.content.Context
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.quickstep.TopTaskTracker
+import com.android.quickstep.TopTaskTracker.CachedTaskInfo
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
 import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_30_70
 import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50
 import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_70_30
+import java.util.function.Consumer
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 class AppPairsControllerTest {
@@ -58,11 +77,38 @@
         appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_70_30)
     }
 
+    @Mock lateinit var mockAppPairIcon: AppPairIcon
+    @Mock lateinit var mockTaskbarActivityContext: TaskbarActivityContext
+    @Mock lateinit var mockTopTaskTracker: TopTaskTracker
+    @Mock lateinit var mockCachedTaskInfo: CachedTaskInfo
+    @Mock lateinit var mockItemInfo1: ItemInfo
+    @Mock lateinit var mockItemInfo2: ItemInfo
+    @Mock lateinit var mockTask1: Task
+    @Mock lateinit var mockTask2: Task
+    @Mock lateinit var mockTaskKey1: TaskKey
+    @Mock lateinit var mockTaskKey2: TaskKey
+    @Captor lateinit var callbackCaptor: ArgumentCaptor<Consumer<Array<Task>>>
+
+    private lateinit var spyAppPairsController: AppPairsController
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
         appPairsController =
             AppPairsController(context, splitSelectStateController, statsLogManager)
+
+        // Stub methods on appPairsController so that they return mocks
+        spyAppPairsController = spy(appPairsController)
+        whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
+        whenever(spyAppPairsController.getTopTaskTracker(mockTaskbarActivityContext))
+            .thenReturn(mockTopTaskTracker)
+        whenever(mockTopTaskTracker.getCachedTopTask(any())).thenReturn(mockCachedTaskInfo)
+        whenever(mockTask1.getKey()).thenReturn(mockTaskKey1)
+        whenever(mockTask2.getKey()).thenReturn(mockTaskKey2)
+        doNothing().whenever(spyAppPairsController).launchAppPair(any())
+        doNothing()
+            .whenever(spyAppPairsController)
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
     }
 
     @Test
@@ -144,4 +190,220 @@
             AppPairsController.convertRankToSnapPosition(right70),
         )
     }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldDoNothingWhenAppsAreAlreadyRunning() {
+        // Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with apps 1 and 2 already on screen
+        whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(1, 2).toIntArray())
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchAppPair and launchToSide were never called
+        verify(spyAppPairsController, never()).launchAppPair(any())
+        verify(spyAppPairsController, never())
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchApp2ToRightWhenApp1IsOnLeft() {
+        // Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with apps 1 and 3 already on screen
+        whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(1, 3).toIntArray())
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchToSide was called with the correct arguments
+        verify(spyAppPairsController, never()).launchAppPair(any())
+        verify(spyAppPairsController, times(1))
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchApp2ToLeftWhenApp1IsOnRight() {
+        // Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with apps 3 and 1 already on screen
+        whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 1).toIntArray())
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchToSide was called with the correct arguments
+        verify(spyAppPairsController, never()).launchAppPair(any())
+        verify(spyAppPairsController, times(1))
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchApp1ToRightWhenApp2IsOnLeft() {
+        // Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with apps 2 and 3 already on screen
+        whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(2, 3).toIntArray())
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchToSide was called with the correct arguments
+        verify(spyAppPairsController, never()).launchAppPair(any())
+        verify(spyAppPairsController, times(1))
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchApp1ToLeftWhenApp2IsOnRight() {
+        // Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with apps 3 and 2 already on screen
+        whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 2).toIntArray())
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchToSide was called with the correct arguments
+        verify(spyAppPairsController, never()).launchAppPair(any())
+        verify(spyAppPairsController, times(1))
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedPairIsOnScreen() {
+        // Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with apps 3 and 4 already on screen
+        whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 4).toIntArray())
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchAppPair was called
+        verify(spyAppPairsController, times(1)).launchAppPair(any())
+        verify(spyAppPairsController, never())
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchApp2ToRightWhenApp1IsFullscreen() {
+        /// Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with app 1 already on screen
+        whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchToSide was called with the correct arguments
+        verify(spyAppPairsController, never()).launchAppPair(any())
+        verify(spyAppPairsController, times(1))
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchApp1ToLeftWhenApp2IsFullscreen() {
+        /// Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with app 2 already on screen
+        whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchToSide was called with the correct arguments
+        verify(spyAppPairsController, never()).launchAppPair(any())
+        verify(spyAppPairsController, times(1))
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() {
+        // Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // ... with app 3 already on screen
+        whenever(mockCachedTaskInfo.taskId).thenReturn(3)
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2)
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchAppPair was called
+        verify(spyAppPairsController, times(1)).launchAppPair(any())
+        verify(spyAppPairsController, never())
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt
new file mode 100644
index 0000000..c190cfe
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.graphics.Rect
+import android.graphics.Region
+import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.IWindowManager
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.quickstep.util.GestureExclusionManager.ExclusionListener
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+
+/** Unit test for [GestureExclusionManager]. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class GestureExclusionManagerTest {
+
+    @Mock private lateinit var windowManager: IWindowManager
+
+    @Mock private lateinit var listener1: ExclusionListener
+    @Mock private lateinit var listener2: ExclusionListener
+
+    private val r1 = Region().apply { union(Rect(0, 0, 100, 200)) }
+    private val r2 = Region().apply { union(Rect(200, 200, 500, 800)) }
+
+    private lateinit var underTest: GestureExclusionManager
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = GestureExclusionManager(windowManager)
+    }
+
+    @Test
+    fun addListener_registers() {
+        underTest.addListener(listener1)
+
+        awaitTasksCompleted()
+        verify(windowManager)
+            .registerSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
+    }
+
+    @Test
+    fun addListener_again_skips_register() {
+        underTest.addListener(listener1)
+        awaitTasksCompleted()
+        reset(windowManager)
+
+        underTest.addListener(listener2)
+
+        awaitTasksCompleted()
+        verifyZeroInteractions(windowManager)
+    }
+
+    @Test
+    fun removeListener_unregisters() {
+        underTest.addListener(listener1)
+        awaitTasksCompleted()
+        reset(windowManager)
+
+        underTest.removeListener(listener1)
+
+        awaitTasksCompleted()
+        verify(windowManager)
+            .unregisterSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
+    }
+
+    @Test
+    fun removeListener_again_skips_unregister() {
+        underTest.addListener(listener1)
+        underTest.addListener(listener2)
+        awaitTasksCompleted()
+        reset(windowManager)
+
+        underTest.removeListener(listener1)
+
+        awaitTasksCompleted()
+        verifyZeroInteractions(windowManager)
+    }
+
+    @Test
+    fun onSystemGestureExclusionChanged_dispatches_to_listeners() {
+        underTest.addListener(listener1)
+        underTest.addListener(listener2)
+        awaitTasksCompleted()
+
+        underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
+        awaitTasksCompleted()
+        verify(listener1).onGestureExclusionChanged(r1, r2)
+        verify(listener2).onGestureExclusionChanged(r1, r2)
+    }
+
+    @Test
+    fun addLister_dispatches_second_time() {
+        underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
+        awaitTasksCompleted()
+        underTest.addListener(listener1)
+        awaitTasksCompleted()
+        verifyZeroInteractions(listener1)
+
+        underTest.addListener(listener2)
+        awaitTasksCompleted()
+
+        verifyZeroInteractions(listener1)
+        verify(listener2).onGestureExclusionChanged(r1, r2)
+    }
+
+    private fun awaitTasksCompleted() {
+        Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+        Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index de152fa..68c9bf9 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -281,7 +281,7 @@
         whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
         doNothing()
             .whenever(spySplitAnimationController)
-            .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+            .composeScaleUpLaunchAnimation(any(), any(), any())
 
         spySplitAnimationController.playSplitLaunchAnimation(
             null /* launchingTaskView */,
@@ -298,8 +298,7 @@
             {} /* finishCallback */
         )
 
-        verify(spySplitAnimationController)
-            .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any())
     }
 
     @Test
diff --git a/res/drawable/icon_menu_background.xml b/res/drawable/icon_menu_background.xml
deleted file mode 100644
index 8a95c3e..0000000
--- a/res/drawable/icon_menu_background.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2023 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="rectangle">
-    <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
-</shape>
\ No newline at end of file
diff --git a/res/drawable/icon_menu_background_corners.xml b/res/drawable/icon_menu_background_corners.xml
deleted file mode 100644
index 16e3fe2..0000000
--- a/res/drawable/icon_menu_background_corners.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2023 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="rectangle">
-    <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
-    <corners android:radius="@dimen/task_thumbnail_icon_menu_corner_radius" />
-</shape>
\ No newline at end of file
diff --git a/res/drawable/icon_menu_elevation_background.xml b/res/drawable/icon_menu_elevation_background.xml
deleted file mode 100644
index 16e3fe2..0000000
--- a/res/drawable/icon_menu_elevation_background.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2023 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="rectangle">
-    <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
-    <corners android:radius="@dimen/task_thumbnail_icon_menu_corner_radius" />
-</shape>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3aa4a77..c187000 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -414,11 +414,6 @@
     <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_start_margin">0dp</dimen>
-    <dimen name="task_thumbnail_icon_menu_background_max_width">0dp</dimen>
-    <dimen name="task_thumbnail_icon_menu_corner_radius">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_edge_padding">0dp</dimen>
     <dimen name="overview_task_margin">0dp</dimen>
@@ -490,4 +485,7 @@
     <dimen name="ps_button_width">36dp</dimen>
     <dimen name="ps_lock_button_width">89dp</dimen>
     <dimen name="ps_app_divider_padding">16dp</dimen>
+
+    <!-- WindowManagerProxy -->
+    <dimen name="max_width_and_height_of_small_display_cutout">136px</dimen>
 </resources>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index bf4f6c3..563dfe2 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -260,7 +260,6 @@
     public int overviewTaskIconSizePx;
     public int overviewTaskIconDrawableSizePx;
     public int overviewTaskIconDrawableSizeGridPx;
-    public int overviewTaskIconAppChipMenuDrawableSizePx;
     public int overviewTaskThumbnailTopMarginPx;
     public final int overviewActionsHeight;
     public final int overviewActionsTopMarginPx;
@@ -686,8 +685,6 @@
                 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size);
         overviewTaskIconDrawableSizeGridPx =
                 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size_grid);
-        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.
@@ -2156,8 +2153,6 @@
                 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/Workspace.java b/src/com/android/launcher3/Workspace.java
index 984a9ae..ac0d7ce 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1636,9 +1636,15 @@
             mDragController.addDragListener(
                     new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
                         @Override
-                        protected void enableAccessibleDrag(boolean enable) {
-                            super.enableAccessibleDrag(enable);
+                        protected void enableAccessibleDrag(boolean enable,
+                                @Nullable DragObject dragObject) {
+                            super.enableAccessibleDrag(enable, dragObject);
                             setEnableForLayout(mLauncher.getHotseat(), enable);
+                            if (enable && dragObject != null
+                                    && dragObject.dragInfo instanceof LauncherAppWidgetInfo) {
+                                mLauncher.getHotseat().setImportantForAccessibility(
+                                        IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+                            }
                         }
                     });
         }
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
index 0d7df2b..79b8187 100644
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
@@ -20,6 +20,8 @@
 import android.view.ViewGroup;
 import android.view.ViewGroup.OnHierarchyChangeListener;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
@@ -50,13 +52,13 @@
     @Override
     public void onDragStart(DragObject dragObject, DragOptions options) {
         mViewGroup.setOnHierarchyChangeListener(this);
-        enableAccessibleDrag(true);
+        enableAccessibleDrag(true, dragObject);
     }
 
     @Override
     public void onDragEnd() {
         mViewGroup.setOnHierarchyChangeListener(null);
-        enableAccessibleDrag(false);
+        enableAccessibleDrag(false, null);
         Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
     }
 
@@ -75,7 +77,7 @@
         }
     }
 
-    protected void enableAccessibleDrag(boolean enable) {
+    protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) {
         for (int i = 0; i < mViewGroup.getChildCount(); i++) {
             setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
         }
diff --git a/src/com/android/launcher3/anim/AnimatedFloat.java b/src/com/android/launcher3/anim/AnimatedFloat.java
index 2f3fa63..b414ab6 100644
--- a/src/com/android/launcher3/anim/AnimatedFloat.java
+++ b/src/com/android/launcher3/anim/AnimatedFloat.java
@@ -109,6 +109,13 @@
     public void cancelAnimation() {
         if (mValueAnimator != null) {
             mValueAnimator.cancel();
+            // Clears the property values, so further ObjectAnimator#setCurrentFraction from e.g.
+            // AnimatorPlaybackController calls would do nothing. The null check is necessary to
+            // avoid mValueAnimator being set to null in onAnimationEnd.
+            if (mValueAnimator != null) {
+                mValueAnimator.setValues();
+                mValueAnimator = null;
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index 586beb2..e58890f 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -83,6 +83,20 @@
         add(anim);
     }
 
+    /**
+     * Add an {@link AnimatedFloat} to the animation.
+     * <p>
+     * Different from {@link #addFloat}, this method use animator provided by
+     * {@link AnimatedFloat#animateToValue}, which tracks the animator inside the AnimatedFloat,
+     * allowing the animation to be canceled and animate again from AnimatedFloat side.
+     */
+    public void addAnimatedFloat(AnimatedFloat target, float from, float to,
+            TimeInterpolator interpolator) {
+        Animator anim = target.animateToValue(from, to);
+        anim.setInterpolator(interpolator);
+        add(anim);
+    }
+
     /** If trace is enabled, add counter to trace animation progress. */
     public void logAnimationProgressToTrace(String counterName) {
         if (Trace.isEnabled()) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 2f3f029..f013126 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -320,8 +320,9 @@
                 mDragController.addDragListener(new AccessibleDragListenerAdapter(
                         mContent, FolderAccessibilityHelper::new) {
                     @Override
-                    protected void enableAccessibleDrag(boolean enable) {
-                        super.enableAccessibleDrag(enable);
+                    protected void enableAccessibleDrag(boolean enable,
+                            @Nullable DragObject dragObject) {
+                        super.enableAccessibleDrag(enable, dragObject);
                         mFooter.setImportantForAccessibility(enable
                                 ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
                                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 31ae7c2..88b98aa 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -433,7 +433,9 @@
                     !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) &&
                         !isSafeMode &&
                         (si == null) &&
-                        (lapi == null)
+                        (lapi == null) &&
+                        !(Utilities.enableSupportForArchiving() &&
+                            pmHelper.isAppArchived(component.packageName))
                 ) {
                     // Restore never started
                     c.markDeleted(
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 50d8886..92288e1 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -107,6 +107,23 @@
     }
 
     /**
+     * Returns whether the target app is in archived state
+     */
+    @SuppressWarnings("NewApi")
+    public boolean isAppArchived(@NonNull final String packageName) {
+        final ApplicationInfo info;
+        try {
+            info = mPm.getPackageInfo(packageName,
+                    PackageManager.PackageInfoFlags.of(
+                            PackageManager.MATCH_ARCHIVED_PACKAGES)).applicationInfo;
+            return info.isArchived;
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Failed to get applicationInfo for package: " + packageName, e);
+            return false;
+        }
+    }
+
+    /**
      * Returns the application info for the provided package or null
      */
     @Nullable
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 32f1736..6a0090c 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -17,6 +17,7 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.launcher3.Utilities.dpToPx;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT;
@@ -49,6 +50,9 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.shared.ResourceUtils;
@@ -130,11 +134,11 @@
         Resources systemRes = context.getResources();
         Configuration config = systemRes.getConfiguration();
 
-        boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
+        boolean isLargeScreen = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
         boolean isGesture = isGestureNav(context);
         boolean isPortrait = config.screenHeightDp > config.screenWidthDp;
 
-        int bottomNav = isTablet
+        int bottomNav = isLargeScreen
                 ? 0
                 : (isPortrait
                         ? getDimenByName(systemRes, NAVBAR_HEIGHT)
@@ -165,6 +169,9 @@
             insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets);
         }
 
+        applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+                context, isLargeScreen, dpToPx(config.screenWidthDp), oldInsets, insetsBuilder);
+
         WindowInsets result = insetsBuilder.build();
         Insets systemWindowInsets = result.getInsetsIgnoringVisibility(
                 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
@@ -173,6 +180,71 @@
         return result;
     }
 
+    /**
+     * For large screen, when display cutout is at bottom left/right corner of screen, override
+     * display cutout's bottom inset to 0, because launcher allows drawing content over that area.
+     */
+    private static void applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+            @NonNull Context context,
+            boolean isLargeScreen,
+            int screenWidthPx,
+            @NonNull WindowInsets windowInsets,
+            @NonNull WindowInsets.Builder insetsBuilder) {
+        if (!isLargeScreen || !Utilities.ATLEAST_S) {
+            return;
+        }
+
+        final DisplayCutout displayCutout = windowInsets.getDisplayCutout();
+        if (displayCutout == null) {
+            return;
+        }
+
+        if (!areBottomDisplayCutoutsSmallAndAtCorners(
+                displayCutout.getBoundingRectBottom(), screenWidthPx, context.getResources())) {
+            return;
+        }
+
+        Insets oldDisplayCutoutInset = windowInsets.getInsets(WindowInsets.Type.displayCutout());
+        Insets newDisplayCutoutInset = Insets.of(
+                oldDisplayCutoutInset.left,
+                oldDisplayCutoutInset.top,
+                oldDisplayCutoutInset.right,
+                0);
+        insetsBuilder.setInsetsIgnoringVisibility(
+                WindowInsets.Type.displayCutout(), newDisplayCutoutInset);
+    }
+
+    /**
+     * @see doc at {@link #areBottomDisplayCutoutsSmallAndAtCorners(Rect, int, int)}
+     */
+    private static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+            @NonNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res) {
+        return areBottomDisplayCutoutsSmallAndAtCorners(cutoutRectBottom, screenWidthPx,
+                res.getDimensionPixelSize(R.dimen.max_width_and_height_of_small_display_cutout));
+    }
+
+    /**
+     * Return true if bottom display cutouts are at bottom left/right corners, AND has width or
+     * height <= maxWidthAndHeightOfSmallCutoutPx. Note that display cutout rect and screenWidthPx
+     * passed to this method should be in the SAME screen rotation.
+     *
+     * @param cutoutRectBottom bottom display cutout rect, this is based on current screen rotation
+     * @param screenWidthPx screen width in px based on current screen rotation
+     * @param maxWidthAndHeightOfSmallCutoutPx maximum width and height pixels of cutout.
+     */
+    @VisibleForTesting
+    static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+            @NonNull Rect cutoutRectBottom, int screenWidthPx,
+            int maxWidthAndHeightOfSmallCutoutPx) {
+        // Empty cutoutRectBottom means there is no display cutout at the bottom. We should ignore
+        // it by returning false.
+        if (cutoutRectBottom.isEmpty()) {
+            return false;
+        }
+        return (cutoutRectBottom.right <= maxWidthAndHeightOfSmallCutoutPx)
+                || cutoutRectBottom.left >= (screenWidthPx - maxWidthAndHeightOfSmallCutoutPx);
+    }
+
     protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) {
         Resources systemRes = context.getResources();
         int statusBarHeight = getDimenByName(systemRes,
@@ -249,6 +321,12 @@
             DisplayCutout rotatedCutout = rotateCutout(
                     displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i);
             Rect insets = getSafeInsets(rotatedCutout);
+            if (areBottomDisplayCutoutsSmallAndAtCorners(
+                    rotatedCutout.getBoundingRectBottom(),
+                    bounds.width(),
+                    context.getResources())) {
+                insets.bottom = 0;
+            }
             insets.top = Math.max(insets.top, statusBarHeight);
             insets.bottom = Math.max(insets.bottom, navBarHeight);
 
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
index 18752e9..af8f67f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
@@ -110,7 +110,6 @@
 	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 c0de8d8..5b83dd7 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
@@ -110,7 +110,6 @@
 	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 361247b..03a0048 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
@@ -110,7 +110,6 @@
 	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 d93ec58..84258b3 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
@@ -110,7 +110,6 @@
 	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 92caf23..87a9700 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
@@ -110,7 +110,6 @@
 	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 3815fa9..169256d 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
@@ -110,7 +110,6 @@
 	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 7e0f316..6ed6879 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -110,7 +110,6 @@
 	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 58c3890..a51669b 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -110,7 +110,6 @@
 	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 1e363a2..61d5ee6 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
@@ -110,7 +110,6 @@
 	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 617b54b..ac73e2f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
@@ -110,7 +110,6 @@
 	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 483b5e7..93ab6f2 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
@@ -110,7 +110,6 @@
 	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 8d0640c..ec9a10e 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
@@ -110,7 +110,6 @@
 	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/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
new file mode 100644
index 0000000..4819388
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util.window
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.window.WindowManagerProxy.areBottomDisplayCutoutsSmallAndAtCorners
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit test for [WindowManagerProxy] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WindowManagerProxyTest {
+
+    private val windowWidthPx = 2000
+
+    private val bottomLeftCutout = Rect(0, 2364, 136, 2500)
+    private val bottomRightCutout = Rect(1864, 2364, 2000, 2500)
+
+    private val bottomLeftCutoutWithOffset = Rect(10, 2364, 136, 2500)
+    private val bottomRightCutoutWithOffset = Rect(1864, 2364, 1990, 2500)
+
+    private val maxWidthAndHeightOfSmallCutoutPx = 136
+
+    @Test
+    fun cutout_at_bottom_right_corner() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomRightCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_left_corner_with_offset() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomLeftCutoutWithOffset,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_right_corner_with_offset() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomRightCutoutWithOffset,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_left_corner() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomLeftCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_edge_at_bottom_corners() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomLeftCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_too_big_not_at_bottom_corners() {
+        // Rect in size of 200px
+        val bigBottomLeftCutout = Rect(0, 2300, 200, 2500)
+
+        assertFalse(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bigBottomLeftCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_too_small_at_bottom_corners() {
+        // Rect in size of 100px
+        val smallBottomLeft = Rect(0, 2400, 100, 2500)
+
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                smallBottomLeft,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_empty_not_at_bottom_corners() {
+        val emptyRect = Rect(0, 0, 0, 0)
+
+        assertFalse(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                emptyRect,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+}