Merge "Skip animating stashed handle color based on color sampling if it's the same" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 0df6c36..be8ebbb 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -332,3 +332,10 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "letter_fast_scroller"
+    namespace: "launcher"
+    description: "Change fast scroller to a lettered list"
+    bug: "358673724"
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index 11740ee..e11b00c 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -31,3 +31,11 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "enable_large_desktop_windowing_tile"
+    namespace: "launcher_overview"
+    description: "Makes the desktop tiles larger and moves them to the front of the list in Overview."
+    bug: "353947137"
+}
+
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index 784a094..d90d916 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -15,22 +15,23 @@
 -->
 <com.android.quickstep.task.thumbnail.TaskThumbnailView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent" >
 
-    <ImageView
+    <com.android.quickstep.views.FixedSizeImageView
         android:id="@+id/task_thumbnail"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:importantForAccessibility="no"
         android:scaleType="matrix"
-        android:visibility="gone"/>
+        android:visibility="invisible"/>
 
     <com.android.quickstep.task.thumbnail.LiveTileView
         android:id="@+id/task_thumbnail_live_tile"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:visibility="gone"/>
+        android:visibility="invisible"/>
 
     <View
         android:id="@+id/task_thumbnail_scrim"
@@ -39,16 +40,23 @@
         android:background="@color/overview_foreground_scrim_color"
         android:alpha="0" />
 
-    <FrameLayout
-        android:id="@+id/splash_container"
+    <View
+        android:id="@+id/splash_background"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:visibility="gone">
-        <ImageView
-            android:id="@+id/splash_icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:importantForAccessibility="no" />
-    </FrameLayout>
+        android:background="@android:color/black"
+        android:alpha="0"
+        android:importantForAccessibility="no" />
+
+    <com.android.quickstep.views.FixedSizeImageView
+        android:id="@+id/splash_icon"
+        android:layout_width="@dimen/task_thumbnail_splash_icon_size"
+        android:layout_height="@dimen/task_thumbnail_splash_icon_size"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:scaleType="fitCenter"
+        android:alpha="0"
+        android:importantForAccessibility="no" />
 </com.android.quickstep.task.thumbnail.TaskThumbnailView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 867ce17..e691134 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -41,6 +41,8 @@
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <!--  The icon size for the focused task, placed in center of touch target  -->
     <dimen name="task_thumbnail_icon_drawable_size">44dp</dimen>
+    <!--  The splash icon size on in Overview when a task is settled in the list -->
+    <dimen name="task_thumbnail_splash_icon_size">52dp</dimen>
     <!--  The border width shown when task is hovered  -->
     <dimen name="task_hover_border_width">4dp</dimen>
     <!--  The space under the focused task icon  -->
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index d973149..e940553 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.util.Log;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.RemoteAnimationTarget;
 
@@ -196,6 +197,7 @@
                 if (skipFirstFrame) {
                     // Because t=0 has the app icon in its original spot, we can skip the
                     // first frame and have the same movement one frame earlier.
+                    Log.d("b/311077782", "LauncherAnimationRunner.setAnimation");
                     mAnimator.setCurrentPlayTime(
                             Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration()));
                 }
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 189deda..f7da34a 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -30,7 +30,7 @@
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.views.DesktopTaskView
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import java.util.function.Consumer
 
 /** Manage recents related operations with desktop tasks */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 1471234..487bc54 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -186,6 +186,7 @@
     private final WindowManager mWindowManager;
     private DeviceProfile mDeviceProfile;
     private WindowManager.LayoutParams mWindowLayoutParams;
+    private WindowManager.LayoutParams mLastUpdatedLayoutParams;
     private boolean mIsFullscreen;
     // The size we should return to when we call setTaskbarWindowFullscreen(false)
     private int mLastRequestedNonFullscreenSize;
@@ -442,6 +443,7 @@
         mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false);
         mLastRequestedNonFullscreenSize = getDefaultTaskbarWindowSize();
         mWindowLayoutParams = createAllWindowParams();
+        mLastUpdatedLayoutParams = new WindowManager.LayoutParams();
 
         // Initialize controllers after all are constructed.
         mControllers.init(sharedState);
@@ -1727,6 +1729,12 @@
 
     void notifyUpdateLayoutParams() {
         if (mDragLayer.isAttachedToWindow()) {
+            // Copy the current windowLayoutParams to mLastUpdatedLayoutParams and compare the diff.
+            // If there is no change, we will skip the call to updateViewLayout.
+            int changes = mLastUpdatedLayoutParams.copyFrom(mWindowLayoutParams);
+            if (changes == 0) {
+                return;
+            }
             if (enableTaskbarNoRecreate()) {
                 mWindowManager.updateViewLayout(mDragLayer.getRootView(), mWindowLayoutParams);
             } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index ff1ea98..221504d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -56,7 +56,6 @@
 import com.android.launcher3.util.Executors
 import java.io.PrintWriter
 import kotlin.jvm.optionals.getOrNull
-import kotlin.math.max
 
 /** Handles the insets that Taskbar provides to underlying apps and the IME. */
 class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController {
@@ -106,7 +105,8 @@
     }
 
     fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() {
-        val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
+        val taskbarStashController = controllers.taskbarStashController
+        val tappableHeight = taskbarStashController.tappableHeightToReportToApps
         // We only report tappableElement height for unstashed, persistent taskbar,
         // which is also when we draw the rounded corners above taskbar.
         val insetsRoundedCornerFlag =
@@ -133,7 +133,7 @@
         }
 
         val bubbleControllers = controllers.bubbleControllers.getOrNull()
-        val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight
+        val taskbarTouchableHeight = taskbarStashController.touchableHeight
         val bubblesTouchableHeight =
             bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0
         // reset touch bounds
@@ -147,12 +147,10 @@
                 defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
             }
         }
-        val taskbarUIController = controllers.uiController as? LauncherTaskbarUIController
-        if (taskbarUIController?.isOnHome != true) {
-            // only add the bars touch region if not on home
-            val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight)
+        if (taskbarStashController.isInApp || taskbarStashController.isInOverview) {
+            // only add the taskbar touch region if not on home
             val bottom = windowLayoutParams.height
-            val top = bottom - touchableHeight
+            val top = bottom - taskbarTouchableHeight
             val right = context.deviceProfile.widthPx
             defaultTouchableRegion.addBoundsToRegion(Rect(/* left= */ 0, top, right, bottom))
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 5c3add2..eb1165a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK;
 import static com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_AWAKE;
@@ -146,6 +147,7 @@
     private MultiProperty mIconAlphaForHome;
     private QuickstepLauncher mLauncher;
 
+    private boolean mIsDestroyed = false;
     private Integer mPrevState;
     private int mState;
     private LauncherState mLauncherState = LauncherState.NORMAL;
@@ -256,6 +258,7 @@
     }
 
     public void onDestroy() {
+        mIsDestroyed = true;
         mCanSyncViews = false;
 
         mIconAlignment.finishAnimation();
@@ -349,8 +352,10 @@
         // interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in
         // when the device is asleep, the second condition extends ensures that the transition from
         // and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar
-        // hide/reveal animation timings.
-        boolean isTaskbarHidden = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING)
+        // hide/reveal animation timings. The Taskbar can show when dreaming if the glanceable hub
+        // is showing on top.
+        boolean isTaskbarHidden = (hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING)
+                && !hasAnyFlag(systemUiStateFlags, SYSUI_STATE_COMMUNAL_HUB_SHOWING))
                 || (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE;
         updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden);
 
@@ -407,7 +412,7 @@
     }
 
     public Animator applyState(long duration, boolean start) {
-        if (mControllers.taskbarActivityContext.isDestroyed()) {
+        if (mIsDestroyed) {
             return null;
         }
         Animator animator = null;
@@ -753,7 +758,7 @@
     }
 
     private void updateIconAlphaForHome(float alpha) {
-        if (mControllers.taskbarActivityContext.isDestroyed()) {
+        if (mIsDestroyed) {
             return;
         }
         mIconAlphaForHome.setValue(alpha);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 8a20131..56f88d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -424,6 +424,11 @@
         return hasAnyFlag(FLAGS_IN_APP);
     }
 
+    /** Returns whether the taskbar is currently in overview screen. */
+    public boolean isInOverview() {
+        return hasAnyFlag(FLAG_IN_OVERVIEW);
+    }
+
     /**
      * Returns the height that taskbar will be touchable.
      */
@@ -1146,7 +1151,8 @@
      */
     public void setUpTaskbarSystemAction(boolean visible) {
         UI_HELPER_EXECUTOR.execute(() -> {
-            if (!visible || !DisplayController.isTransientTaskbar(mActivity)) {
+            if (!visible || !DisplayController.isTransientTaskbar(mActivity)
+                    || mActivity.isPhoneMode()) {
                 mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR);
                 mIsTaskbarSystemActionRegistered = false;
                 return;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index fc76972..54a7fdc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -108,9 +108,15 @@
     private long mAllAppsButtonTouchDelayMs;
     private boolean mAllAppsTouchTriggered;
 
-    // Only non-null when device supports having an All Apps button.
+    // Only non-null when device supports having an All Apps button, or Recent Apps.
     private @Nullable IconButtonView mTaskbarDivider;
 
+    /**
+     * Whether the divider is between Hotseat icons and Recents,
+     * instead of between All Apps button and Hotseat.
+     */
+    private boolean mAddedDividerForRecents;
+
     private final View mQsb;
 
     private final float mTransientTaskbarMinWidth;
@@ -340,7 +346,7 @@
     protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
         int nextViewIndex = 0;
         int numViewsAnimated = 0;
-        boolean addedDividerForRecents = false;
+        mAddedDividerForRecents = false;
 
         if (mAllAppsButton != null) {
             removeView(mAllAppsButton);
@@ -435,7 +441,7 @@
 
         if (mTaskbarDivider != null && !recentTasks.isEmpty()) {
             addView(mTaskbarDivider, nextViewIndex++);
-            addedDividerForRecents = true;
+            mAddedDividerForRecents = true;
         }
 
         // Add Recent/Running icons.
@@ -499,10 +505,10 @@
         }
 
         if (mAllAppsButton != null) {
-            addView(mAllAppsButton, mIsRtl ? getChildCount() : 0);
+            addView(mAllAppsButton, mIsRtl ? hotseatItemInfos.length : 0);
 
             // If there are no recent tasks, add divider after All Apps (unless it's the only view).
-            if (!addedDividerForRecents && mTaskbarDivider != null && getChildCount() > 1) {
+            if (!mAddedDividerForRecents && mTaskbarDivider != null && getChildCount() > 1) {
                 addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1);
             }
         }
@@ -734,6 +740,14 @@
     }
 
     /**
+     * Returns whether the divider is between Hotseat icons and Recents,
+     * instead of between All Apps button and Hotseat.
+     */
+    public boolean isDividerForRecents() {
+        return mAddedDividerForRecents;
+    }
+
+    /**
      * Returns the QSB in the taskbar.
      */
     public View getQsb() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index b8b85d1..aef21aa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -100,6 +100,12 @@
     public static final int ALPHA_INDEX_BUBBLE_BAR = 7;
     private static final int NUM_ALPHA_CHANNELS = 8;
 
+    /** Only used for animation purposes, to position the divider between two item indices. */
+    public static final float DIVIDER_VIEW_POSITION_OFFSET = 0.5f;
+
+    /** Used if an unexpected edge case is hit in {@link #getPositionInHotseat}. */
+    private static final float ERROR_POSITION_IN_HOTSEAT_NOT_FOUND = -100;
+
     private static boolean sEnableModelLoadingForTests = true;
 
     private final TaskbarActivityContext mActivity;
@@ -144,6 +150,7 @@
     private Runnable mOnControllerPreCreateCallback = NO_OP;
 
     // Stored here as signals to determine if the mIconAlignController needs to be recreated.
+    private boolean mIsIconAlignedWithHotseat;
     private boolean mIsHotseatIconOnTopWhenAligned;
     private boolean mIsStashed;
 
@@ -681,15 +688,17 @@
             mIconAlignControllerLazy = null;
             return;
         }
-
         boolean isHotseatIconOnTopWhenAligned =
                 mControllers.uiController.isHotseatIconOnTopWhenAligned();
+        boolean isIconAlignedWithHotseat = mControllers.uiController.isIconAlignedWithHotseat();
         boolean isStashed = mControllers.taskbarStashController.isStashed();
-        // Re-create animation when mIsHotseatIconOnTopWhenAligned or mIsStashed changes.
+        // Re-create animation when any of these values change.
         if (mIconAlignControllerLazy == null
                 || mIsHotseatIconOnTopWhenAligned != isHotseatIconOnTopWhenAligned
+                || mIsIconAlignedWithHotseat != isIconAlignedWithHotseat
                 || mIsStashed != isStashed) {
             mIsHotseatIconOnTopWhenAligned = isHotseatIconOnTopWhenAligned;
+            mIsIconAlignedWithHotseat = isIconAlignedWithHotseat;
             mIsStashed = isStashed;
             mIconAlignControllerLazy = createIconAlignmentController(launcherDp);
         }
@@ -741,17 +750,22 @@
                 ? mTransientTaskbarDp.taskbarBottomMargin
                 : mPersistentTaskbarDp.taskbarBottomMargin;
 
+        int firstRecentTaskIndex = -1;
         for (int i = 0; i < mTaskbarView.getChildCount(); i++) {
             View child = mTaskbarView.getChildAt(i);
             boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonView();
             boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerView();
+            boolean isRecentTask = child.getTag() instanceof GroupTask;
+            // TODO(b/343522351): show recents on the home screen.
+            final boolean isRecentsInHotseat = false;
             if (!mIsHotseatIconOnTopWhenAligned) {
                 // When going to home, the EMPHASIZED interpolator in TaskbarLauncherStateController
                 // plays iconAlignment to 1 really fast, therefore moving the fading towards the end
                 // to avoid icons disappearing rather than fading out visually.
                 setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0.8f, 1f));
             } else if ((isAllAppsButton && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get())
-                    || (isTaskbarDividerView && enableTaskbarPinning())) {
+                    || (isTaskbarDividerView && enableTaskbarPinning())
+                    || (isRecentTask && !isRecentsInHotseat)) {
                 if (!isToHome
                         && mIsHotseatIconOnTopWhenAligned
                         && mIsStashed) {
@@ -812,25 +826,17 @@
                 continue;
             }
 
-            float positionInHotseat;
-            if (isAllAppsButton) {
-                // Note that there is no All Apps button in the hotseat,
-                // this position is only used as its convenient for animation purposes.
-                positionInHotseat = Utilities.isRtl(child.getResources())
-                        ? taskbarDp.numShownHotseatIcons
-                        : -1;
-            }  else if (isTaskbarDividerView) {
-                // Note that there is no taskbar divider view in the hotseat,
-                // this position is only used as its convenient for animation purposes.
-                positionInHotseat = Utilities.isRtl(child.getResources())
-                        ? taskbarDp.numShownHotseatIcons - 0.5f
-                        : -0.5f;
-            } else if (child.getTag() instanceof ItemInfo) {
-                positionInHotseat = ((ItemInfo) child.getTag()).screenId;
-            } else {
-                Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child);
-                continue;
+            int recentTaskIndex = -1;
+            if (isRecentTask) {
+                if (firstRecentTaskIndex < 0) {
+                    firstRecentTaskIndex = i;
+                }
+                recentTaskIndex = i - firstRecentTaskIndex;
             }
+            float positionInHotseat = getPositionInHotseat(taskbarDp.numShownHotseatIcons, child,
+                    mIsRtl, isAllAppsButton, isTaskbarDividerView,
+                    mTaskbarView.isDividerForRecents(), recentTaskIndex);
+            if (positionInHotseat == ERROR_POSITION_IN_HOTSEAT_NOT_FOUND) continue;
 
             float hotseatAdjustedBorderSpace =
                     launcherDp.getHotseatAdjustedBorderSpaceForBubbleBar(child.getContext());
@@ -866,6 +872,58 @@
         return controller;
     }
 
+    /**
+     * Returns the index of the given child relative to its position in hotseat.
+     * Examples:
+     * -1 is the item before the first hotseat item.
+     * -0.5 is between those (e.g. for the divider).
+     * {@link #ERROR_POSITION_IN_HOTSEAT_NOT_FOUND} if there's no calculation relative to hotseat.
+     */
+    @VisibleForTesting
+    float getPositionInHotseat(int numShownHotseatIcons, View child, boolean isRtl,
+            boolean isAllAppsButton, boolean isTaskbarDividerView, boolean isDividerForRecents,
+            int recentTaskIndex) {
+        float positionInHotseat;
+        // Note that there is no All Apps button in the hotseat,
+        // this position is only used as it's convenient for animation purposes.
+        float allAppsButtonPositionInHotseat = isRtl
+                // Right after all hotseat items.
+                // [HHHHHH]|[>A<]
+                ? numShownHotseatIcons
+                // Right before all hotseat items.
+                // [>A<]|[HHHHHH]
+                : -1;
+        // Note that there are no recent tasks in the hotseat,
+        // this position is only used as it's convenient for animation purposes.
+        float firstRecentTaskPositionInHotseat = isRtl
+                // After all hotseat icons and All Apps button.
+                // [HHHHHH][A]|[>R<R]
+                ? numShownHotseatIcons + 1
+                // Right after all hotseat items.
+                // [A][HHHHHH]|[>R<R]
+                : numShownHotseatIcons;
+        if (isAllAppsButton) {
+            positionInHotseat = allAppsButtonPositionInHotseat;
+        }  else if (isTaskbarDividerView) {
+            // Note that there is no taskbar divider view in the hotseat,
+            // this position is only used as it's convenient for animation purposes.
+            float relativePosition = isDividerForRecents
+                    ? firstRecentTaskPositionInHotseat
+                    : allAppsButtonPositionInHotseat;
+            positionInHotseat = relativePosition > 0
+                    ? relativePosition - DIVIDER_VIEW_POSITION_OFFSET
+                    : relativePosition + DIVIDER_VIEW_POSITION_OFFSET;
+        } else if (child.getTag() instanceof ItemInfo) {
+            positionInHotseat = ((ItemInfo) child.getTag()).screenId;
+        } else if (recentTaskIndex >= 0) {
+            positionInHotseat = firstRecentTaskPositionInHotseat + recentTaskIndex;
+        } else {
+            Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child);
+            return ERROR_POSITION_IN_HOTSEAT_NOT_FOUND;
+        }
+        return positionInHotseat;
+    }
+
     private boolean bubbleBarHasBubbles() {
         return mControllers.bubbleControllers.isPresent()
                 && mControllers.bubbleControllers.get().bubbleBarViewController.hasBubbles();
@@ -950,7 +1008,6 @@
                 "ALPHA_INDEX_RECENTS_DISABLED",
                 "ALPHA_INDEX_NOTIFICATION_EXPANDED",
                 "ALPHA_INDEX_ASSISTANT_INVOKED",
-                "ALPHA_INDEX_IME_BUTTON_NAV",
                 "ALPHA_INDEX_SMALL_SCREEN");
 
         mModelCallbacks.dumpLogs(prefix + "\t", pw);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index a36d5f0..cdd3e13 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -496,6 +496,11 @@
                 () -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
     }
 
+    /** Notifies WMShell to show the expanded view. */
+    void showExpandedView() {
+        mSystemUiProxy.showExpandedView();
+    }
+
     //
     // Loading data for the bubbles
     //
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 1f0851f..c458936 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -244,7 +244,7 @@
                     if (mIsBarExpanded && mSelectedBubbleView != null) {
                         mSelectedBubbleView.markSeen();
                     }
-                    updateWidth();
+                    updateLayoutParams();
                 },
                 /* onUpdate= */ animator -> {
                     updateBubblesLayoutProperties(mBubbleBarLocation);
@@ -733,7 +733,7 @@
 
                 @Override
                 public void onAnimationEnd() {
-                    updateWidth();
+                    updateLayoutParams();
                     mBubbleAnimator = null;
                 }
 
@@ -791,7 +791,7 @@
             @Override
             public void onAnimationEnd() {
                 removeView(removedBubble);
-                updateWidth();
+                updateLayoutParams();
                 mBubbleAnimator = null;
                 if (onEndRunnable != null) {
                     onEndRunnable.run();
@@ -823,7 +823,7 @@
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         super.addView(child, index, params);
-        updateWidth();
+        updateLayoutParams();
         updateBubbleAccessibilityStates();
         updateContentDescription();
     }
@@ -887,7 +887,7 @@
             mSelectedBubbleView = null;
             mBubbleBarBackground.showArrow(false);
         }
-        updateWidth();
+        updateLayoutParams();
         updateBubbleAccessibilityStates();
         updateContentDescription();
         mDismissedByDragBubbleView = null;
@@ -937,12 +937,6 @@
         }
     }
 
-    private void updateWidth() {
-        LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
-        lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth());
-        setLayoutParams(lp);
-    }
-
     private void updateLayoutParams() {
         LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
         lp.height = (int) getBubbleBarExpandedHeight();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 1d74b28..5c1a546 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -119,7 +119,8 @@
         mBubbleDragController = bubbleControllers.bubbleDragController;
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
-        mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
+        mBubbleBarViewAnimator = new BubbleBarViewAnimator(
+                mBarView, mBubbleStashController, mBubbleBarController::showExpandedView);
         mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
         onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 2ed88d8..99c50f2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -36,6 +36,7 @@
 constructor(
     private val bubbleBarView: BubbleBarView,
     private val bubbleStashController: BubbleStashController,
+    private val onExpanded: Runnable,
     private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
 ) {
 
@@ -406,7 +407,7 @@
         springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
         springBackAnimation.addEndListener { _, _, _, _, _, _, _ ->
             if (animatingBubble?.expand == true) {
-                bubbleBarView.isExpanded = true
+                expandBubbleBar()
                 cancelHideAnimation()
             } else {
                 moveToState(AnimatingBubble.State.IN)
@@ -417,7 +418,7 @@
         ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
             .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
             .withEndAction {
-                if (animatingBubble?.expand == true) bubbleBarView.isExpanded = true
+                if (animatingBubble?.expand == true) expandBubbleBar()
                 springBackAnimation.start()
             }
             .start()
@@ -451,7 +452,7 @@
         this.animatingBubble = animatingBubble.copy(expand = true)
         // if we're fully in and waiting to hide, cancel the hide animation and clean up
         if (animatingBubble.state == AnimatingBubble.State.IN) {
-            bubbleBarView.isExpanded = true
+            expandBubbleBar()
             cancelHideAnimation()
         }
     }
@@ -489,6 +490,11 @@
         this.animatingBubble = animatingBubble.copy(state = state)
     }
 
+    private fun expandBubbleBar() {
+        bubbleBarView.isExpanded = true
+        onExpanded.run()
+    }
+
     /**
      * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands
      * as part of the animation, the expansion should start after the bubble bar reaches the peak
@@ -510,7 +516,7 @@
             }
             val expand = animatingBubble?.expand ?: false
             if (reachedPeak && expand && !startedExpanding) {
-                bubbleBarView.isExpanded = true
+                expandBubbleBar()
                 startedExpanding = true
             }
             previousTy = ty
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 110ca16..37e0034 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ArgbEvaluator;
 import android.animation.Keyframe;
@@ -39,6 +40,7 @@
 import android.os.Process;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Property;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
@@ -71,12 +73,27 @@
  */
 public class PredictedAppIcon extends DoubleShadowBubbleTextView {
 
+    private static final float RING_SCALE_START_VALUE = 0.75f;
     private static final int RING_SHADOW_COLOR = 0x99000000;
     private static final float RING_EFFECT_RATIO = 0.095f;
 
     private static final long ICON_CHANGE_ANIM_DURATION = 360;
     private static final long ICON_CHANGE_ANIM_STAGGER = 50;
 
+    private static final Property<PredictedAppIcon, Float> RING_SCALE_PROPERTY =
+            new Property<>(Float.TYPE, "ringScale") {
+                @Override
+                public Float get(PredictedAppIcon icon) {
+                    return icon.mRingScale;
+                }
+
+                @Override
+                public void set(PredictedAppIcon icon, Float value) {
+                    icon.mRingScale = value;
+                    icon.invalidate();
+                }
+            };
+
     boolean mIsDrawingDot = false;
     private final DeviceProfile mDeviceProfile;
     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -96,6 +113,11 @@
     private Animator mSlotMachineAnim;
     private float mSlotMachineIconTranslationY;
 
+    // Used to animate the "ring" around predicted icons
+    private float mRingScale = 1f;
+    private boolean mForceHideRing = false;
+    private Animator mRingScaleAnim;
+
     private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y =
             new FloatProperty<PredictedAppIcon>("slotMachineTranslationY") {
         @Override
@@ -356,17 +378,57 @@
         }
     }
 
+    @Override
+    public void setIconVisible(boolean visible) {
+        setForceHideRing(!visible);
+        super.setIconVisible(visible);
+    }
+
+    private void setForceHideRing(boolean forceHideRing) {
+        if (mForceHideRing == forceHideRing) {
+            return;
+        }
+        mForceHideRing = forceHideRing;
+
+        if (forceHideRing) {
+            invalidate();
+        } else {
+            animateRingScale(RING_SCALE_START_VALUE, 1);
+        }
+    }
+
+    private void cancelRingScaleAnim() {
+        if (mRingScaleAnim != null) {
+            mRingScaleAnim.cancel();
+        }
+    }
+
+    private void animateRingScale(float... ringScale) {
+        cancelRingScaleAnim();
+        mRingScaleAnim = ObjectAnimator.ofFloat(this, RING_SCALE_PROPERTY, ringScale);
+        mRingScaleAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mRingScaleAnim = null;
+            }
+        });
+        mRingScaleAnim.start();
+    }
+
     private void drawEffect(Canvas canvas) {
-        // Don't draw ring effect if item is about to be dragged.
-        if (mDrawForDrag) {
+        // Don't draw ring effect if item is about to be dragged or if the icon is not visible.
+        if (mDrawForDrag || !mIsIconVisible) {
             return;
         }
         mIconRingPaint.setColor(RING_SHADOW_COLOR);
         mIconRingPaint.setMaskFilter(mShadowFilter);
+        int count = canvas.save();
+        canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
         canvas.drawPath(mRingPath, mIconRingPaint);
         mIconRingPaint.setColor(mPlateColor);
         mIconRingPaint.setMaskFilter(null);
         canvas.drawPath(mRingPath, mIconRingPaint);
+        canvas.restoreToCount(count);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index eafc5b6..202276e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -181,7 +181,7 @@
                         // - The task is snapped
                         mAllowGoingDown = i == mRecentsView.getCurrentPage()
                                 && DisplayController.getNavigationMode(mContainer).hasGestures
-                                && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask())
+                                && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isLargeTile())
                                 && mRecentsView.isTaskInExpectedScrollPosition(i);
 
                         directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection;
@@ -310,7 +310,7 @@
                 // Set mOverrideVelocity to control task dismiss velocity in onDragEnd
                 int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity;
                 if (mRecentsView.showAsGrid()) {
-                    if (mTaskBeingDragged.isFocusedTask()) {
+                    if (mTaskBeingDragged.isLargeTile()) {
                         velocityDimenId =
                                 R.dimen.default_task_dismiss_drag_velocity_grid_focus_task;
                     } else {
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index fbc0d14..94f4920 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -24,8 +24,8 @@
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskContainer
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 
 /** A menu item, "Desktop", that allows the user to bring the current app into Desktop Windowing. */
 class DesktopSystemShortcut(
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
deleted file mode 100644
index 8f533a3..0000000
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ /dev/null
@@ -1,529 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.PagedView.INVALID_PAGE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.BinderThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.internal.jank.Cuj;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.taskbar.TaskbarUIController;
-import com.android.launcher3.util.RunnableList;
-import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.RecentsViewContainer;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-/**
- * Helper class to handle various atomic commands for switching between Overview.
- */
-public class OverviewCommandHelper {
-    private static final String TAG = "OverviewCommandHelper";
-
-    public static final int TYPE_SHOW = 1;
-    public static final int TYPE_KEYBOARD_INPUT = 2;
-    public static final int TYPE_HIDE = 3;
-    public static final int TYPE_TOGGLE = 4;
-    public static final int TYPE_HOME = 5;
-
-    /**
-     * Use case for needing a queue is double tapping recents button in 3 button nav.
-     * Size of 2 should be enough. We'll toss in one more because we're kind hearted.
-     */
-    private final static int MAX_QUEUE_SIZE = 3;
-
-    private static final String TRANSITION_NAME = "Transition:toOverview";
-
-    private final TouchInteractionService mService;
-    private final OverviewComponentObserver mOverviewComponentObserver;
-    private final TaskAnimationManager mTaskAnimationManager;
-    private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>();
-
-    /**
-     * Index of the TaskView that should be focused when launching Overview. Persisted so that we
-     * do not lose the focus across multiple calls of
-     * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command
-     */
-    private int mKeyboardTaskFocusIndex = -1;
-
-    /**
-     * Whether we should incoming toggle commands while a previous toggle command is still ongoing.
-     * This serves as a rate-limiter to prevent overlapping animations that can clobber each other
-     * and prevent clean-up callbacks from running. This thus prevents a recurring set of bugs with
-     * janky recents animations and unresponsive home and overview buttons.
-     */
-    private boolean mWaitForToggleCommandComplete = false;
-
-    public OverviewCommandHelper(TouchInteractionService service,
-            OverviewComponentObserver observer,
-            TaskAnimationManager taskAnimationManager) {
-        mService = service;
-        mOverviewComponentObserver = observer;
-        mTaskAnimationManager = taskAnimationManager;
-    }
-
-    /**
-     * Called when the command finishes execution.
-     */
-    private void scheduleNextTask(CommandInfo command) {
-        if (mPendingCommands.isEmpty()) {
-            Log.d(TAG, "no pending commands to schedule");
-            return;
-        }
-        if (mPendingCommands.get(0) != command) {
-            Log.d(TAG, "next task not scheduled."
-                    + " mPendingCommands[0] type is " + mPendingCommands.get(0)
-                    + " - command type is: " + command);
-            return;
-        }
-        Log.d(TAG, "scheduleNextTask called: " + command);
-        mPendingCommands.remove(0);
-        executeNext();
-    }
-
-    /**
-     * Executes the next command from the queue. If the command finishes immediately (returns true),
-     * it continues to execute the next command, until the queue is empty of a command defer's its
-     * completion (returns false).
-     */
-    @UiThread
-    private void executeNext() {
-        if (mPendingCommands.isEmpty()) {
-            Log.d(TAG, "executeNext - mPendingCommands is empty");
-            return;
-        }
-        CommandInfo cmd = mPendingCommands.get(0);
-
-        boolean result = executeCommand(cmd);
-        Log.d(TAG, "executeNext cmd type: " + cmd + ", result: " + result);
-        if (result) {
-            scheduleNextTask(cmd);
-        }
-    }
-
-    @UiThread
-    private void addCommand(CommandInfo cmd) {
-        boolean wasEmpty = mPendingCommands.isEmpty();
-        mPendingCommands.add(cmd);
-        if (wasEmpty) {
-            executeNext();
-        }
-    }
-
-    /**
-     * Adds a command to be executed next, after all pending tasks are completed.
-     * Max commands that can be queued is {@link #MAX_QUEUE_SIZE}.
-     * Requests after reaching that limit will be silently dropped.
-     */
-    @BinderThread
-    public void addCommand(int type) {
-        if (mPendingCommands.size() >= MAX_QUEUE_SIZE) {
-            Log.d(TAG, "the pending command queue is full (" + mPendingCommands.size() + "). "
-                    + "command not added: " + type);
-            return;
-        }
-        Log.d(TAG, "adding command type: " + type);
-        CommandInfo cmd = new CommandInfo(type);
-        MAIN_EXECUTOR.execute(() -> addCommand(cmd));
-    }
-
-    @UiThread
-    public void clearPendingCommands() {
-        Log.d(TAG, "clearing pending commands - size: " + mPendingCommands.size());
-        mPendingCommands.clear();
-    }
-
-    @UiThread
-    public boolean canStartHomeSafely() {
-        return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME;
-    }
-
-    @Nullable
-    private TaskView getNextTask(RecentsView view) {
-        final TaskView runningTaskView = view.getRunningTaskView();
-
-        if (runningTaskView == null) {
-            return view.getTaskViewAt(0);
-        } else {
-            final TaskView nextTask = view.getNextTaskView();
-            return nextTask != null ? nextTask : runningTaskView;
-        }
-    }
-
-    private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) {
-        RunnableList callbackList = null;
-        if (taskView != null) {
-            mWaitForToggleCommandComplete = true;
-            taskView.setEndQuickSwitchCuj(true);
-            callbackList = taskView.launchTasks();
-        }
-
-        if (callbackList != null) {
-            callbackList.add(() -> {
-                Log.d(TAG, "launching task callback: " + cmd);
-                scheduleNextTask(cmd);
-                mWaitForToggleCommandComplete = false;
-            });
-            Log.d(TAG, "launching task - waiting for callback: " + cmd);
-            return false;
-        } else {
-            recents.startHome();
-            mWaitForToggleCommandComplete = false;
-            return true;
-        }
-    }
-
-    /**
-     * Executes the task and returns true if next task can be executed. If false, then the next
-     * task is deferred until {@link #scheduleNextTask} is called
-     */
-    private <T extends StatefulActivity<?> & RecentsViewContainer> boolean executeCommand(
-            CommandInfo cmd) {
-        if (mWaitForToggleCommandComplete && cmd.type == TYPE_TOGGLE) {
-            Log.d(TAG, "executeCommand: " + cmd
-                    + " - waiting for toggle command complete");
-            return true;
-        }
-        BaseActivityInterface<?, T> activityInterface =
-                mOverviewComponentObserver.getActivityInterface();
-
-        RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
-        RecentsView<?, ?> createdRecentsView;
-
-        Log.d(TAG, "executeCommand: " + cmd
-                + " - visibleRecentsView: " + visibleRecentsView);
-        if (visibleRecentsView == null) {
-            T activity = activityInterface.getCreatedContainer();
-            createdRecentsView = activity == null ? null : activity.getOverviewPanel();
-            DeviceProfile dp = activity == null ? null : activity.getDeviceProfile();
-            TaskbarUIController uiController = activityInterface.getTaskbarController();
-            boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
-                    && uiController != null
-                    && dp != null
-                    && (dp.isTablet || dp.isTwoPanels);
-
-            switch (cmd.type) {
-                case TYPE_HIDE:
-                    if (!allowQuickSwitch) {
-                        return true;
-                    }
-                    mKeyboardTaskFocusIndex = uiController.launchFocusedTask();
-                    if (mKeyboardTaskFocusIndex == -1) {
-                        return true;
-                    }
-                    break;
-                case TYPE_KEYBOARD_INPUT:
-                    if (allowQuickSwitch) {
-                        uiController.openQuickSwitchView();
-                        return true;
-                    } else {
-                        mKeyboardTaskFocusIndex = 0;
-                        break;
-                    }
-                case TYPE_HOME:
-                    ActiveGestureLog.INSTANCE.addLog(
-                            "OverviewCommandHelper.executeCommand(TYPE_HOME)");
-                    // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
-                    // we should still call it on main thread because launcher is waiting for
-                    // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
-                    // could potentially delay resuming launcher. See b/348668521 for more details.
-                    mService.startActivity(mOverviewComponentObserver.getHomeIntent());
-                    return true;
-                case TYPE_SHOW:
-                    // When Recents is not currently visible, the command's type is TYPE_SHOW
-                    // when overview is triggered via the keyboard overview button or Action+Tab
-                    // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button
-                    // nav is TYPE_TOGGLE.
-                    mKeyboardTaskFocusIndex = 0;
-                    break;
-                default:
-                    // continue below to handle displaying Recents.
-            }
-        } else {
-            createdRecentsView = visibleRecentsView;
-            switch (cmd.type) {
-                case TYPE_SHOW:
-                    // already visible
-                    return true;
-                case TYPE_KEYBOARD_INPUT: {
-                    if (visibleRecentsView.isHandlingTouch()) {
-                        return true;
-                    }
-                }
-                case TYPE_HIDE: {
-                    if (visibleRecentsView.isHandlingTouch()) {
-                        return true;
-                    }
-                    mKeyboardTaskFocusIndex = INVALID_PAGE;
-                    int currentPage = visibleRecentsView.getNextPage();
-                    TaskView tv = (currentPage >= 0
-                            && currentPage < visibleRecentsView.getTaskViewCount())
-                            ? (TaskView) visibleRecentsView.getPageAt(currentPage)
-                            : null;
-                    return launchTask(visibleRecentsView, tv, cmd);
-                }
-                case TYPE_TOGGLE:
-                    return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd);
-                case TYPE_HOME:
-                    visibleRecentsView.startHome();
-                    return true;
-            }
-        }
-
-        if (createdRecentsView != null) {
-            createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex);
-        }
-        // Handle recents view focus when launching from home
-        Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
-
-            @Override
-            public void onAnimationStart(Animator animation) {
-                super.onAnimationStart(animation);
-                updateRecentsViewFocus(cmd);
-                logShowOverviewFrom(cmd.type);
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                Log.d(TAG, "switching to Overview state - onAnimationEnd: " + cmd);
-                super.onAnimationEnd(animation);
-                onRecentsViewFocusUpdated(cmd);
-                scheduleNextTask(cmd);
-            }
-        };
-        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
-            Log.d(TAG, "switching to Overview state - waiting: " + cmd);
-            // If successfully switched, wait until animation finishes
-            return false;
-        }
-
-        final T activity = activityInterface.getCreatedContainer();
-        if (activity != null) {
-            InteractionJankMonitorWrapper.begin(
-                    activity.getRootView(),
-                    Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
-        }
-
-        GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE,
-                GestureState.TrackpadGestureType.NONE);
-        gestureState.setHandlingAtomicEvent(true);
-        AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory()
-                .newHandler(gestureState, cmd.createTime);
-        interactionHandler.setGestureEndCallback(
-                () -> onTransitionComplete(cmd, interactionHandler));
-        interactionHandler.initWhenReady("OverviewCommandHelper: cmd.type=" + cmd.type);
-
-        RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() {
-            @Override
-            public void onRecentsAnimationStart(RecentsAnimationController controller,
-                    RecentsAnimationTargets targets) {
-                updateRecentsViewFocus(cmd);
-                logShowOverviewFrom(cmd.type);
-                activityInterface.runOnInitBackgroundStateUI(() ->
-                        interactionHandler.onGestureEnded(0, new PointF()));
-                cmd.removeListener(this);
-            }
-
-            @Override
-            public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
-                interactionHandler.onGestureCancelled();
-                cmd.removeListener(this);
-
-                T createdActivity = activityInterface.getCreatedContainer();
-                if (createdActivity == null) {
-                    return;
-                }
-                if (createdRecentsView != null) {
-                    createdRecentsView.onRecentsAnimationComplete();
-                }
-            }
-        };
-
-        if (visibleRecentsView != null) {
-            visibleRecentsView.moveRunningTaskToFront();
-        }
-        if (mTaskAnimationManager.isRecentsAnimationRunning()) {
-            cmd.mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(gestureState);
-            cmd.mActiveCallbacks.addListener(interactionHandler);
-            mTaskAnimationManager.notifyRecentsAnimationState(interactionHandler);
-            interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/);
-
-            cmd.mActiveCallbacks.addListener(recentAnimListener);
-            mTaskAnimationManager.notifyRecentsAnimationState(recentAnimListener);
-        } else {
-            Intent intent = new Intent(interactionHandler.getLaunchIntent());
-            intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, gestureState.getGestureId());
-            cmd.mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(
-                    gestureState, intent, interactionHandler);
-            interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/);
-            cmd.mActiveCallbacks.addListener(recentAnimListener);
-        }
-        Trace.beginAsyncSection(TRANSITION_NAME, 0);
-        Log.d(TAG, "switching via recents animation - onGestureStarted: " + cmd);
-        return false;
-    }
-
-    private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
-        Log.d(TAG, "switching via recents animation - onTransitionComplete: " + cmd);
-        cmd.removeListener(handler);
-        Trace.endAsyncSection(TRANSITION_NAME, 0);
-        onRecentsViewFocusUpdated(cmd);
-        scheduleNextTask(cmd);
-    }
-
-    private void updateRecentsViewFocus(CommandInfo cmd) {
-        RecentsView recentsView =
-                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
-        if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE
-                && cmd.type != TYPE_SHOW)) {
-            return;
-        }
-        // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT),
-        // the touch mode somehow is not change to false by the Android framework.
-        // The subsequent tab to go through tasks in overview can only be dispatched to
-        // focuses views, while focus can only be requested in
-        // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
-        // here we launch overview with live tile.
-        recentsView.getViewRootImpl().touchModeChanged(false);
-        // Ensure that recents view has focus so that it receives the followup key inputs
-        if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) {
-            return;
-        }
-        if (requestFocus(recentsView.getNextTaskView())) {
-            return;
-        }
-        if (requestFocus(recentsView.getTaskViewAt(0))) {
-            return;
-        }
-        requestFocus(recentsView);
-    }
-
-    private void onRecentsViewFocusUpdated(CommandInfo cmd) {
-        RecentsView recentsView =
-                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
-        if (recentsView == null
-                || cmd.type != TYPE_HIDE
-                || mKeyboardTaskFocusIndex == INVALID_PAGE) {
-            return;
-        }
-        recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE);
-        recentsView.setCurrentPage(mKeyboardTaskFocusIndex);
-        mKeyboardTaskFocusIndex = INVALID_PAGE;
-    }
-
-    private boolean requestFocus(@Nullable View taskView) {
-        if (taskView == null) {
-            return false;
-        }
-        taskView.post(() -> {
-            taskView.requestFocus();
-            taskView.requestAccessibilityFocus();
-        });
-        return true;
-    }
-
-    private <T extends StatefulActivity<?> & RecentsViewContainer>
-            void logShowOverviewFrom(int cmdType) {
-        BaseActivityInterface<?, T> activityInterface =
-                mOverviewComponentObserver.getActivityInterface();
-        var container = activityInterface.getCreatedContainer();
-        if (container != null) {
-            StatsLogManager.LauncherEvent event;
-            switch (cmdType) {
-                case TYPE_SHOW -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT;
-                case TYPE_HIDE ->
-                        event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH;
-                case TYPE_TOGGLE -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON;
-                default -> {
-                    return;
-                }
-            }
-
-            StatsLogManager.newInstance(container.asContext())
-                    .logger()
-                    .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
-                            .setTaskSwitcherContainer(
-                                    LauncherAtom.TaskSwitcherContainer.getDefaultInstance())
-                            .build())
-                    .log(event);
-        }
-    }
-
-    public void dump(PrintWriter pw) {
-        pw.println("OverviewCommandHelper:");
-        pw.println("  mPendingCommands=" + mPendingCommands.size());
-        if (!mPendingCommands.isEmpty()) {
-            pw.println("    pendingCommandType=" + mPendingCommands.get(0).type);
-        }
-        pw.println("  mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex);
-        pw.println("  mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete);
-    }
-
-    private static class CommandInfo {
-        public final long createTime = SystemClock.elapsedRealtime();
-        public final int type;
-        RecentsAnimationCallbacks mActiveCallbacks;
-
-        CommandInfo(int type) {
-            this.type = type;
-        }
-
-        void removeListener(RecentsAnimationListener listener) {
-            if (mActiveCallbacks != null) {
-                mActiveCallbacks.removeListener(listener);
-            }
-        }
-
-        @NonNull
-        @Override
-        public String toString() {
-            return "CommandInfo("
-                    + "type=" + type + ", "
-                    + "createTime=" + createTime + ", "
-                    + "mActiveCallbacks=" + mActiveCallbacks
-                    + ")";
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
new file mode 100644
index 0000000..f6b9e4e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.content.Intent
+import android.graphics.PointF
+import android.os.SystemClock
+import android.os.Trace
+import android.util.Log
+import android.view.View
+import androidx.annotation.BinderThread
+import androidx.annotation.UiThread
+import com.android.internal.jank.Cuj
+import com.android.launcher3.PagedView
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.logger.LauncherAtom
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.*
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.RunnableList
+import com.android.quickstep.util.ActiveGestureLog
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.quickstep.views.TaskView
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper
+import java.io.PrintWriter
+
+/** Helper class to handle various atomic commands for switching between Overview. */
+class OverviewCommandHelper(
+    private val touchInteractionService: TouchInteractionService,
+    private val overviewComponentObserver: OverviewComponentObserver,
+    private val taskAnimationManager: TaskAnimationManager
+) {
+    private val pendingCommands = mutableListOf<CommandInfo>()
+
+    /**
+     * Index of the TaskView that should be focused when launching Overview. Persisted so that we do
+     * not lose the focus across multiple calls of [OverviewCommandHelper.executeCommand] for the
+     * same command
+     */
+    private var keyboardTaskFocusIndex = -1
+
+    /**
+     * Whether we should incoming toggle commands while a previous toggle command is still ongoing.
+     * This serves as a rate-limiter to prevent overlapping animations that can clobber each other
+     * and prevent clean-up callbacks from running. This thus prevents a recurring set of bugs with
+     * janky recents animations and unresponsive home and overview buttons.
+     */
+    private var waitForToggleCommandComplete = false
+
+    /** Called when the command finishes execution. */
+    private fun scheduleNextTask(command: CommandInfo) {
+        if (pendingCommands.isEmpty()) {
+            Log.d(TAG, "no pending commands to schedule")
+            return
+        }
+        if (pendingCommands.first() !== command) {
+            Log.d(
+                TAG,
+                "next task not scheduled. First pending command type " +
+                    "is ${pendingCommands.first()} - command type is: $command"
+            )
+            return
+        }
+        Log.d(TAG, "scheduleNextTask called: $command")
+        pendingCommands.removeFirst()
+        executeNext()
+    }
+
+    /**
+     * Executes the next command from the queue. If the command finishes immediately (returns true),
+     * it continues to execute the next command, until the queue is empty of a command defer's its
+     * completion (returns false).
+     */
+    @UiThread
+    private fun executeNext() {
+        if (pendingCommands.isEmpty()) {
+            Log.d(TAG, "executeNext - pendingCommands is empty")
+            return
+        }
+        val command = pendingCommands.first()
+        val result = executeCommand(command)
+        Log.d(TAG, "executeNext command type: $command, result: $result")
+        if (result) {
+            scheduleNextTask(command)
+        }
+    }
+
+    @UiThread
+    private fun addCommand(command: CommandInfo) {
+        val wasEmpty = pendingCommands.isEmpty()
+        pendingCommands.add(command)
+        if (wasEmpty) {
+            executeNext()
+        }
+    }
+
+    /**
+     * Adds a command to be executed next, after all pending tasks are completed. Max commands that
+     * can be queued is [.MAX_QUEUE_SIZE]. Requests after reaching that limit will be silently
+     * dropped.
+     */
+    @BinderThread
+    fun addCommand(type: Int) {
+        if (pendingCommands.size >= MAX_QUEUE_SIZE) {
+            Log.d(
+                TAG,
+                "the pending command queue is full (${pendingCommands.size}). command not added: $type"
+            )
+            return
+        }
+        Log.d(TAG, "adding command type: $type")
+        val command = CommandInfo(type)
+        Executors.MAIN_EXECUTOR.execute { addCommand(command) }
+    }
+
+    @UiThread
+    fun clearPendingCommands() {
+        Log.d(TAG, "clearing pending commands - size: ${pendingCommands.size}")
+        pendingCommands.clear()
+    }
+
+    @UiThread
+    fun canStartHomeSafely(): Boolean =
+        pendingCommands.isEmpty() || pendingCommands.first().type == TYPE_HOME
+
+    private fun getNextTask(view: RecentsView<*, *>): TaskView? {
+        val runningTaskView = view.runningTaskView
+
+        return if (runningTaskView == null) {
+            view.getTaskViewAt(0)
+        } else {
+            val nextTask = view.nextTaskView
+            nextTask ?: runningTaskView
+        }
+    }
+
+    private fun launchTask(
+        recents: RecentsView<*, *>,
+        taskView: TaskView?,
+        command: CommandInfo
+    ): Boolean {
+        var callbackList: RunnableList? = null
+        if (taskView != null) {
+            waitForToggleCommandComplete = true
+            taskView.isEndQuickSwitchCuj = true
+            callbackList = taskView.launchTasks()
+        }
+
+        if (callbackList != null) {
+            callbackList.add {
+                Log.d(TAG, "launching task callback: $command")
+                scheduleNextTask(command)
+                waitForToggleCommandComplete = false
+            }
+            Log.d(TAG, "launching task - waiting for callback: $command")
+            return false
+        } else {
+            recents.startHome()
+            waitForToggleCommandComplete = false
+            return true
+        }
+    }
+
+    /**
+     * Executes the task and returns true if next task can be executed. If false, then the next task
+     * is deferred until [.scheduleNextTask] is called
+     */
+    private fun executeCommand(command: CommandInfo): Boolean {
+        if (waitForToggleCommandComplete && command.type == TYPE_TOGGLE) {
+            Log.d(TAG, "executeCommand: $command - waiting for toggle command complete")
+            return true
+        }
+        val activityInterface: BaseActivityInterface<*, *> =
+            overviewComponentObserver.activityInterface
+
+        val visibleRecentsView: RecentsView<*, *>? =
+            activityInterface.getVisibleRecentsView<RecentsView<*, *>>()
+        val createdRecentsView: RecentsView<*, *>?
+
+        Log.d(TAG, "executeCommand: $command - visibleRecentsView: $visibleRecentsView")
+        if (visibleRecentsView == null) {
+            val activity = activityInterface.getCreatedContainer() as? RecentsViewContainer
+            createdRecentsView = activity?.getOverviewPanel()
+            val deviceProfile = activity?.getDeviceProfile()
+            val uiController = activityInterface.getTaskbarController()
+            val allowQuickSwitch =
+                FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() &&
+                    uiController != null &&
+                    deviceProfile != null &&
+                    (deviceProfile.isTablet || deviceProfile.isTwoPanels)
+
+            when (command.type) {
+                TYPE_HIDE -> {
+                    if (!allowQuickSwitch) return true
+                    keyboardTaskFocusIndex = uiController!!.launchFocusedTask()
+                    if (keyboardTaskFocusIndex == -1) return true
+                }
+                TYPE_KEYBOARD_INPUT ->
+                    if (allowQuickSwitch) {
+                        uiController!!.openQuickSwitchView()
+                        return true
+                    } else {
+                        keyboardTaskFocusIndex = 0
+                    }
+                TYPE_HOME -> {
+                    ActiveGestureLog.INSTANCE.addLog(
+                        "OverviewCommandHelper.executeCommand(TYPE_HOME)"
+                    )
+                    // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
+                    // we should still call it on main thread because launcher is waiting for
+                    // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
+                    // could potentially delay resuming launcher. See b/348668521 for more details.
+                    touchInteractionService.startActivity(overviewComponentObserver.homeIntent)
+                    return true
+                }
+                TYPE_SHOW ->
+                    // When Recents is not currently visible, the command's type is
+                    // TYPE_SHOW
+                    // when overview is triggered via the keyboard overview button or Action+Tab
+                    // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button
+                    // nav is TYPE_TOGGLE.
+                    keyboardTaskFocusIndex = 0
+                else -> {}
+            }
+        } else {
+            createdRecentsView = visibleRecentsView
+            when (command.type) {
+                TYPE_SHOW -> return true // already visible
+                TYPE_KEYBOARD_INPUT,
+                TYPE_HIDE -> {
+                    if (visibleRecentsView.isHandlingTouch) return true
+
+                    keyboardTaskFocusIndex = PagedView.INVALID_PAGE
+                    val currentPage = visibleRecentsView.nextPage
+                    val taskView = visibleRecentsView.getTaskViewAt(currentPage)
+                    return launchTask(visibleRecentsView, taskView, command)
+                }
+                TYPE_TOGGLE ->
+                    return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), command)
+                TYPE_HOME -> {
+                    visibleRecentsView.startHome()
+                    return true
+                }
+            }
+        }
+
+        createdRecentsView?.setKeyboardTaskFocusIndex(keyboardTaskFocusIndex)
+        // Handle recents view focus when launching from home
+        val animatorListener: Animator.AnimatorListener =
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animation: Animator) {
+                    super.onAnimationStart(animation)
+                    updateRecentsViewFocus(command)
+                    logShowOverviewFrom(command.type)
+                }
+
+                override fun onAnimationEnd(animation: Animator) {
+                    Log.d(TAG, "switching to Overview state - onAnimationEnd: $command")
+                    super.onAnimationEnd(animation)
+                    onRecentsViewFocusUpdated(command)
+                    scheduleNextTask(command)
+                }
+            }
+        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
+            Log.d(TAG, "switching to Overview state - waiting: $command")
+            // If successfully switched, wait until animation finishes
+            return false
+        }
+
+        val activity = activityInterface.getCreatedContainer()
+        if (activity != null) {
+            InteractionJankMonitorWrapper.begin(activity.rootView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
+        }
+
+        val gestureState =
+            touchInteractionService.createGestureState(
+                GestureState.DEFAULT_STATE,
+                GestureState.TrackpadGestureType.NONE
+            )
+        gestureState.isHandlingAtomicEvent = true
+        val interactionHandler =
+            touchInteractionService.swipeUpHandlerFactory.newHandler(
+                gestureState,
+                command.createTime
+            )
+        interactionHandler.setGestureEndCallback {
+            onTransitionComplete(command, interactionHandler)
+        }
+        interactionHandler.initWhenReady("OverviewCommandHelper: command.type=${command.type}")
+
+        val recentAnimListener: RecentsAnimationCallbacks.RecentsAnimationListener =
+            object : RecentsAnimationCallbacks.RecentsAnimationListener {
+                override fun onRecentsAnimationStart(
+                    controller: RecentsAnimationController,
+                    targets: RecentsAnimationTargets
+                ) {
+                    updateRecentsViewFocus(command)
+                    logShowOverviewFrom(command.type)
+                    activityInterface.runOnInitBackgroundStateUI {
+                        interactionHandler.onGestureEnded(0f, PointF())
+                    }
+                    command.removeListener(this)
+                }
+
+                override fun onRecentsAnimationCanceled(
+                    thumbnailDatas: HashMap<Int, ThumbnailData>
+                ) {
+                    interactionHandler.onGestureCancelled()
+                    command.removeListener(this)
+
+                    activityInterface.getCreatedContainer() ?: return
+                    createdRecentsView?.onRecentsAnimationComplete()
+                }
+            }
+
+        // TODO(b/361768912): Dead code. Remove or update after this bug is fixed.
+        //        if (visibleRecentsView != null) {
+        //            visibleRecentsView.moveRunningTaskToFront();
+        //        }
+
+        if (taskAnimationManager.isRecentsAnimationRunning) {
+            command.setAnimationCallbacks(
+                taskAnimationManager.continueRecentsAnimation(gestureState)
+            )
+            command.addListener(interactionHandler)
+            taskAnimationManager.notifyRecentsAnimationState(interactionHandler)
+            interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/)
+
+            command.addListener(recentAnimListener)
+            taskAnimationManager.notifyRecentsAnimationState(recentAnimListener)
+        } else {
+            val intent =
+                Intent(interactionHandler.launchIntent)
+                    .putExtra(ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID, gestureState.gestureId)
+            command.setAnimationCallbacks(
+                taskAnimationManager.startRecentsAnimation(gestureState, intent, interactionHandler)
+            )
+            interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/)
+            command.addListener(recentAnimListener)
+        }
+        Trace.beginAsyncSection(TRANSITION_NAME, 0)
+        Log.d(TAG, "switching via recents animation - onGestureStarted: $command")
+        return false
+    }
+
+    private fun onTransitionComplete(command: CommandInfo, handler: AbsSwipeUpHandler<*, *, *>) {
+        Log.d(TAG, "switching via recents animation - onTransitionComplete: $command")
+        command.removeListener(handler)
+        Trace.endAsyncSection(TRANSITION_NAME, 0)
+        onRecentsViewFocusUpdated(command)
+        scheduleNextTask(command)
+    }
+
+    private fun updateRecentsViewFocus(command: CommandInfo) {
+        val recentsView: RecentsView<*, *> =
+            overviewComponentObserver.activityInterface.getVisibleRecentsView() ?: return
+        if (
+            command.type != TYPE_KEYBOARD_INPUT &&
+                command.type != TYPE_HIDE &&
+                command.type != TYPE_SHOW
+        ) {
+            return
+        }
+
+        // When the overview is launched via alt tab (command type is TYPE_KEYBOARD_INPUT),
+        // the touch mode somehow is not change to false by the Android framework.
+        // The subsequent tab to go through tasks in overview can only be dispatched to
+        // focuses views, while focus can only be requested in
+        // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
+        // here we launch overview with live tile.
+        recentsView.viewRootImpl.touchModeChanged(false)
+        // Ensure that recents view has focus so that it receives the followup key inputs
+        if (requestFocus(recentsView.getTaskViewAt(keyboardTaskFocusIndex))) return
+        if (requestFocus(recentsView.nextTaskView)) return
+        if (requestFocus(recentsView.getTaskViewAt(0))) return
+        requestFocus(recentsView)
+    }
+
+    private fun onRecentsViewFocusUpdated(command: CommandInfo) {
+        val recentsView: RecentsView<*, *> =
+            overviewComponentObserver.activityInterface.getVisibleRecentsView() ?: return
+        if (command.type != TYPE_HIDE || keyboardTaskFocusIndex == PagedView.INVALID_PAGE) {
+            return
+        }
+        recentsView.setKeyboardTaskFocusIndex(PagedView.INVALID_PAGE)
+        recentsView.currentPage = keyboardTaskFocusIndex
+        keyboardTaskFocusIndex = PagedView.INVALID_PAGE
+    }
+
+    private fun requestFocus(taskView: View?): Boolean {
+        if (taskView == null) return false
+        taskView.post {
+            taskView.requestFocus()
+            taskView.requestAccessibilityFocus()
+        }
+        return true
+    }
+
+    private fun logShowOverviewFrom(commandType: Int) {
+        val activityInterface: BaseActivityInterface<*, *> =
+            overviewComponentObserver.activityInterface
+        val container = activityInterface.getCreatedContainer() as? RecentsViewContainer ?: return
+        val event =
+            when (commandType) {
+                TYPE_SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
+                TYPE_HIDE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
+                TYPE_TOGGLE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
+                else -> return
+            }
+        StatsLogManager.newInstance(container.asContext())
+            .logger()
+            .withContainerInfo(
+                LauncherAtom.ContainerInfo.newBuilder()
+                    .setTaskSwitcherContainer(
+                        LauncherAtom.TaskSwitcherContainer.getDefaultInstance()
+                    )
+                    .build()
+            )
+            .log(event)
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.println("OverviewCommandHelper:")
+        pw.println("  pendingCommands=${pendingCommands.size}")
+        if (pendingCommands.isNotEmpty()) {
+            pw.println("    pendingCommandType=${pendingCommands.first().type}")
+        }
+        pw.println("  mKeyboardTaskFocusIndex=$keyboardTaskFocusIndex")
+        pw.println("  mWaitForToggleCommandComplete=$waitForToggleCommandComplete")
+    }
+
+    private data class CommandInfo(
+        val type: Int,
+        val createTime: Long = SystemClock.elapsedRealtime(),
+        private var animationCallbacks: RecentsAnimationCallbacks? = null
+    ) {
+        fun setAnimationCallbacks(recentsAnimationCallbacks: RecentsAnimationCallbacks) {
+            this.animationCallbacks = recentsAnimationCallbacks
+        }
+
+        fun addListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener) {
+            animationCallbacks?.addListener(listener)
+        }
+
+        fun removeListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener?) {
+            animationCallbacks?.removeListener(listener)
+        }
+    }
+
+    companion object {
+        private const val TAG = "OverviewCommandHelper"
+
+        const val TYPE_SHOW: Int = 1
+        const val TYPE_KEYBOARD_INPUT: Int = 2
+        const val TYPE_HIDE: Int = 3
+        const val TYPE_TOGGLE: Int = 4
+        const val TYPE_HOME: Int = 5
+
+        /**
+         * Use case for needing a queue is double tapping recents button in 3 button nav. Size of 2
+         * should be enough. We'll toss in one more because we're kind hearted.
+         */
+        private const val MAX_QUEUE_SIZE = 3
+
+        private const val TRANSITION_NAME = "Transition:toOverview"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index e66da52..05bef35 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -20,7 +20,7 @@
 
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM;
 
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
@@ -40,8 +40,8 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.recents.IRecentTasksListener;
+import com.android.wm.shell.shared.GroupedRecentTaskInfo;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
-import com.android.wm.shell.util.GroupedRecentTaskInfo;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 3dec381..1be60de 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -17,7 +17,7 @@
 package com.android.quickstep;
 
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.wm.shell.util.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
+import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
 
 import android.app.WindowConfiguration;
 import android.content.Context;
@@ -33,7 +33,7 @@
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
-import com.android.wm.shell.util.SplitBounds;
+import com.android.wm.shell.shared.split.SplitBounds;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index ead5b7b..4392255 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -82,7 +82,6 @@
 import com.android.wm.shell.bubbles.IBubbles;
 import com.android.wm.shell.bubbles.IBubblesListener;
 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.common.pip.IPip;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
 import com.android.wm.shell.desktopmode.IDesktopMode;
@@ -91,16 +90,18 @@
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.recents.IRecentTasks;
 import com.android.wm.shell.recents.IRecentTasksListener;
+import com.android.wm.shell.shared.GroupedRecentTaskInfo;
 import com.android.wm.shell.shared.IShellTransitions;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
+import com.android.wm.shell.shared.split.SplitBounds;
 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
 import com.android.wm.shell.splitscreen.ISplitScreen;
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
 import com.android.wm.shell.splitscreen.ISplitSelectListener;
 import com.android.wm.shell.startingsurface.IStartingWindow;
 import com.android.wm.shell.startingsurface.IStartingWindowListener;
-import com.android.wm.shell.util.GroupedRecentTaskInfo;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -934,6 +935,17 @@
         }
     }
 
+    /** Tells SysUI to show the expanded view. */
+    public void showExpandedView() {
+        try {
+            if (mBubbles != null) {
+                mBubbles.showExpandedView();
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call showExpandedView");
+        }
+    }
+
     //
     // Splitscreen
     //
@@ -1517,7 +1529,7 @@
                 // Aidl bundles need to explicitly set class loader
                 // https://developer.android.com/guide/components/aidl#Bundles
                 if (extras != null) {
-                    extras.setClassLoader(getClass().getClassLoader());
+                    extras.setClassLoader(SplitBounds.class.getClassLoader());
                 }
                 listener.onAnimationStart(new RecentsAnimationControllerCompat(controller), apps,
                         wallpapers, homeContentInsets, minimizedHomeBounds, extras);
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index dad34ac..9e6e2f3 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -315,12 +315,12 @@
             boolean isTaskSplitNotSupported = !task.isDockable ||
                     (intentFlags & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
             boolean hideForExistingMultiWindow = container.getDeviceProfile().isMultiWindowMode;
-            boolean isFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
+            boolean isLargeTile = deviceProfile.isTablet && taskView.isLargeTile();
             boolean isTaskInExpectedScrollPosition =
                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
 
             if (notEnoughTasksToSplit || isTaskSplitNotSupported || hideForExistingMultiWindow
-                    || (isFocusedTask && isTaskInExpectedScrollPosition)) {
+                    || (isLargeTile && isTaskInExpectedScrollPosition)) {
                 return null;
             }
 
@@ -340,11 +340,11 @@
             DeviceProfile deviceProfile = container.getDeviceProfile();
             final TaskView taskView = taskContainer.getTaskView();
             final RecentsView recentsView = taskView.getRecentsView();
-            boolean isLargeTileFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
+            boolean isLargeTile = deviceProfile.isTablet && taskView.isLargeTile();
             boolean isInExpectedScrollPosition =
                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
             boolean shouldShowActionsButtonInstead =
-                    isLargeTileFocusedTask && isInExpectedScrollPosition;
+                    isLargeTile && isInExpectedScrollPosition;
 
             // No "save app pair" menu item if:
             // - we are in 3p launcher
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 3ca7191..14f47d1 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -277,7 +277,7 @@
     }
 
     private void endRemoteAnimation() {
-        if (mRecentsAnimationController != null) {
+        if (!mHomeLaunched && mRecentsAnimationController != null) {
             mRecentsAnimationController.finishController(
                     false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
         }
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
index feed2fd..d2cb595 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
@@ -23,6 +23,4 @@
  */
 data class RecentsDeviceProfile(
     val isLargeScreen: Boolean,
-    val widthPx: Int,
-    val heightPx: Int,
 )
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
index ce39ff1..c64453d 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
@@ -26,7 +26,5 @@
     RecentsDeviceProfileRepository {
 
     override fun getRecentsDeviceProfile() =
-        with(container.deviceProfile) {
-            RecentsDeviceProfile(isLargeScreen = isTablet, widthPx = widthPx, heightPx = heightPx)
-        }
+        with(container.deviceProfile) { RecentsDeviceProfile(isLargeScreen = isTablet) }
 }
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index d8156b1..0b672d1 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -27,7 +27,6 @@
 import com.android.quickstep.recents.usecase.GetThumbnailUseCase
 import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.GetSplashSizeUseCase
 import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
 import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
 import com.android.quickstep.task.viewmodel.TaskContainerData
@@ -176,7 +175,6 @@
                         getThumbnailPositionUseCase = inject(),
                         tasksRepository = inject(),
                         splashAlphaUseCase = inject(scopeId),
-                        getSplashSizeUseCase = inject(scopeId),
                     )
                 TaskOverlayViewModel::class.java -> {
                     val task = extras["Task"] as Task
@@ -204,12 +202,6 @@
                         tasksRepository = inject(),
                         rotationStateRepository = inject(),
                     )
-                GetSplashSizeUseCase::class.java ->
-                    GetSplashSizeUseCase(
-                        taskThumbnailViewData = inject(scopeId),
-                        taskViewData = inject(scopeId, extras),
-                        deviceProfileRepository = inject(),
-                    )
                 else -> {
                     log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
                     error("Factory for ${modelClass.simpleName} not defined!")
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt
deleted file mode 100644
index 145957a..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.thumbnail
-
-import android.graphics.Point
-import android.graphics.drawable.Drawable
-import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
-import com.android.quickstep.task.viewmodel.TaskViewData
-import kotlin.math.min
-
-class GetSplashSizeUseCase(
-    private val taskThumbnailViewData: TaskThumbnailViewData,
-    private val taskViewData: TaskViewData,
-    private val deviceProfileRepository: RecentsDeviceProfileRepository,
-) {
-    fun execute(splashImage: Drawable): Point {
-        val recentsDeviceProfile = deviceProfileRepository.getRecentsDeviceProfile()
-        val screenWidth = recentsDeviceProfile.widthPx
-        val screenHeight = recentsDeviceProfile.heightPx
-        val scaleAtFullscreen =
-            min(
-                screenWidth / taskThumbnailViewData.width.value,
-                screenHeight / taskThumbnailViewData.height.value,
-            )
-        val scaleFactor: Float = 1f / taskViewData.nonGridScale.value / scaleAtFullscreen
-        return Point(
-            (splashImage.intrinsicWidth * scaleFactor / taskThumbnailViewData.scaleX.value).toInt(),
-            (splashImage.intrinsicHeight * scaleFactor / taskThumbnailViewData.scaleY.value)
-                .toInt(),
-        )
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index a6be9f6..36a86f2 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -17,7 +17,6 @@
 package com.android.quickstep.task.thumbnail
 
 import android.graphics.Bitmap
-import android.graphics.Point
 import android.graphics.drawable.Drawable
 import android.view.Surface
 import androidx.annotation.ColorInt
@@ -31,7 +30,7 @@
 
     data class SnapshotSplash(
         val snapshot: Snapshot,
-        val splash: Splash,
+        val splash: Drawable?,
     ) : TaskThumbnailUiState()
 
     data class Snapshot(
@@ -39,9 +38,4 @@
         @Surface.Rotation val thumbnailRotation: Int,
         @ColorInt val backgroundColor: Int
     )
-
-    data class Splash(
-        val icon: Drawable?,
-        val size: Point,
-    )
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 41aee52..0279818 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -24,11 +24,9 @@
 import android.util.AttributeSet
 import android.view.View
 import android.view.ViewOutlineProvider
-import android.widget.FrameLayout
-import android.widget.ImageView
 import androidx.annotation.ColorInt
-import androidx.core.view.isVisible
-import androidx.core.view.updateLayoutParams
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isInvisible
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.util.ViewPool
@@ -41,6 +39,7 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.quickstep.util.TaskCornerRadius
+import com.android.quickstep.views.FixedSizeImageView
 import com.android.systemui.shared.system.QuickStepContract
 import kotlin.math.abs
 import kotlinx.coroutines.CoroutineName
@@ -51,7 +50,7 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 
-class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
+class TaskThumbnailView : ConstraintLayout, ViewPool.Reusable {
 
     private val viewData: TaskThumbnailViewData by RecentsDependencies.inject(this)
     private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)
@@ -60,9 +59,9 @@
 
     private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
     private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
-    private val thumbnailView: ImageView by lazy { findViewById(R.id.task_thumbnail) }
-    private val splashContainer: FrameLayout by lazy { findViewById(R.id.splash_container) }
-    private val splashIcon: ImageView by lazy { findViewById(R.id.splash_icon) }
+    private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) }
+    private val splashBackground: View by lazy { findViewById(R.id.splash_background) }
+    private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) }
 
     private var uiState: TaskThumbnailUiState = Uninitialized
     private var inheritedScale: Float = 1f
@@ -107,7 +106,10 @@
             .onEach { dimProgress -> scrimView.alpha = dimProgress }
             .launchIn(viewAttachedScope)
         viewModel.splashAlpha
-            .onEach { splashAlpha -> splashContainer.alpha = splashAlpha }
+            .onEach { splashAlpha ->
+                splashBackground.alpha = splashAlpha
+                splashIcon.alpha = splashAlpha
+            }
             .launchIn(viewAttachedScope)
         viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope)
         viewModel.inheritedScale
@@ -152,15 +154,13 @@
 
     override fun setScaleX(scaleX: Float) {
         super.setScaleX(scaleX)
-        viewData.scaleX.value = scaleX
-        // Splash icon should ignore scale
+        // Splash icon should ignore scale on TTV
         splashIcon.scaleX = 1 / scaleX
     }
 
     override fun setScaleY(scaleY: Float) {
         super.setScaleY(scaleY)
-        viewData.scaleY.value = scaleY
-        // Splash icon should ignore scale
+        // Splash icon should ignore scale on TTV
         splashIcon.scaleY = 1 / scaleY
     }
 
@@ -173,9 +173,10 @@
     }
 
     private fun resetViews() {
-        liveTileView.isVisible = false
-        thumbnailView.isVisible = false
-        splashContainer.alpha = 0f
+        liveTileView.isInvisible = true
+        thumbnailView.isInvisible = true
+        splashBackground.alpha = 0f
+        splashIcon.alpha = 0f
         scrimView.alpha = 0f
         setBackgroundColor(Color.BLACK)
     }
@@ -185,25 +186,20 @@
     }
 
     private fun drawLiveWindow() {
-        liveTileView.isVisible = true
+        liveTileView.isInvisible = false
     }
 
     private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) {
         drawSnapshot(snapshotSplash.snapshot)
 
-        splashContainer.isVisible = true
-        splashContainer.setBackgroundColor(snapshotSplash.snapshot.backgroundColor)
-        splashIcon.setImageDrawable(snapshotSplash.splash.icon)
-        splashIcon.updateLayoutParams<LayoutParams> {
-            width = snapshotSplash.splash.size.x
-            height = snapshotSplash.splash.size.y
-        }
+        splashBackground.setBackgroundColor(snapshotSplash.snapshot.backgroundColor)
+        splashIcon.setImageDrawable(snapshotSplash.splash)
     }
 
     private fun drawSnapshot(snapshot: Snapshot) {
         drawBackground(snapshot.backgroundColor)
         thumbnailView.setImageBitmap(snapshot.bitmap)
-        thumbnailView.isVisible = true
+        thumbnailView.isInvisible = false
         setImageMatrix()
     }
 
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
index 1f8c0bc..3502029 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
@@ -21,6 +21,4 @@
 class TaskThumbnailViewData {
     val width = MutableStateFlow(0)
     val height = MutableStateFlow(0)
-    val scaleX = MutableStateFlow(1f)
-    val scaleY = MutableStateFlow(1f)
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index 4e29840..b1bb65e 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -19,20 +19,17 @@
 import android.annotation.ColorInt
 import android.app.ActivityTaskManager.INVALID_TASK_ID
 import android.graphics.Matrix
-import android.graphics.Point
 import androidx.core.graphics.ColorUtils
 import com.android.quickstep.recents.data.RecentTasksRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState
 import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.GetSplashSizeUseCase
 import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Splash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.systemui.shared.recents.model.Task
 import kotlin.math.max
@@ -55,7 +52,6 @@
     private val tasksRepository: RecentTasksRepository,
     private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
     private val splashAlphaUseCase: SplashAlphaUseCase,
-    private val getSplashSizeUseCase: GetSplashSizeUseCase,
 ) {
     private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
     private val splashProgress = MutableStateFlow(flowOf(0f))
@@ -100,7 +96,7 @@
                     isBackgroundOnly(taskVal) ->
                         BackgroundOnly(taskVal.colorBackground.removeAlpha())
                     isSnapshotSplashState(taskVal) ->
-                        SnapshotSplash(createSnapshotState(taskVal), createSplashState(taskVal))
+                        SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
                     else -> Uninitialized
                 }
             }
@@ -139,12 +135,6 @@
         return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
     }
 
-    private fun createSplashState(task: Task): Splash {
-        val taskIcon = task.icon
-        val size = if (taskIcon == null) Point() else getSplashSizeUseCase.execute(taskIcon)
-        return Splash(taskIcon, size)
-    }
-
     @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
 
     private companion object {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
index 07dfc29..7a9ecf2 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
@@ -23,8 +23,6 @@
     // This is typically a View concern but it is used to invalidate rendering in other Views
     val scale = MutableStateFlow(1f)
 
-    val nonGridScale = MutableStateFlow(1f)
-
     // TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM.
     /** Whether outline of TaskView is formed by outline thumbnail view(s). */
     val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
index 30ee360..ec75d59 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
@@ -22,8 +22,4 @@
     fun updateScale(scale: Float) {
         taskViewData.scale.value = scale
     }
-
-    fun updateNonGridScale(nonGridScale: Float) {
-        taskViewData.nonGridScale.value = nonGridScale
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index ac4032c..e1013db 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -242,8 +242,7 @@
         WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp();
         ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
         ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
-        mSplitSelectStateController.setLaunchingCuj(cuj);
-        InteractionJankMonitorWrapper.begin(appPairIcon, cuj);
+        mSplitSelectStateController.setLaunchingCuj(appPairIcon, cuj);
 
         mSplitSelectStateController.findLastActiveTasksAndRunCallback(
                 Arrays.asList(app1Key, app2Key),
diff --git a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
new file mode 100644
index 0000000..0a01d8b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
@@ -0,0 +1,71 @@
+/*
+ * 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 com.android.launcher3.Flags.enableLargeDesktopWindowingTile
+import com.android.quickstep.views.DesktopTaskView
+import com.android.quickstep.views.TaskView
+import com.android.quickstep.views.TaskViewType
+
+/**
+ * Helper class for [com.android.quickstep.views.RecentsView]. This util class contains refactored
+ * and extracted functions from RecentsView to facilitate the implementation of unit tests.
+ */
+class RecentsViewUtils {
+
+    /**
+     * Sort task groups to move desktop tasks to the end of the list.
+     *
+     * @param tasks List of group tasks to be sorted.
+     * @return Sorted list of GroupTasks to be used in the RecentsView.
+     */
+    fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
+        val (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+        return otherTasks + desktopTasks
+    }
+
+    fun getFocusedTaskIndex(taskGroups: List<GroupTask>): Int {
+        // The focused task index is placed after the desktop tasks views.
+        return if (enableLargeDesktopWindowingTile()) {
+            taskGroups.count { it.taskViewType == TaskViewType.DESKTOP }
+        } else {
+            0
+        }
+    }
+
+    /**
+     * Counts [numChildren] that are [DesktopTaskView] instances.
+     *
+     * @param numChildren Quantity of children to transverse
+     * @param getTaskViewAt Function that provides a TaskView given an index
+     */
+    fun getDesktopTaskViewCount(numChildren: Int, getTaskViewAt: (Int) -> TaskView?): Int =
+        (0 until numChildren).count { getTaskViewAt(it) is DesktopTaskView }
+
+    /**
+     * Returns the first TaskView that should be displayed as a large tile.
+     *
+     * @param numChildren Quantity of children to transverse
+     * @param getTaskViewAt Function that provides a TaskView given an index
+     */
+    fun getFirstLargeTaskView(numChildren: Int, getTaskViewAt: (Int) -> TaskView?): TaskView? {
+        return (0 until numChildren).firstNotNullOfOrNull { index ->
+            val taskView = getTaskViewAt(index)
+            if (taskView?.isLargeTile == true) taskView else null
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index d58cb91..d982e81 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -17,14 +17,13 @@
 package com.android.quickstep.util
 
 import android.util.Log
-import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.TransitionInfo
 import android.window.TransitionInfo.Change
 import android.window.TransitionInfo.FLAG_FIRST_CUSTOM
 import com.android.launcher3.util.SplitConfigurationOptions
-import com.android.wm.shell.util.SplitBounds
+import com.android.wm.shell.shared.split.SplitBounds
 import java.lang.IllegalStateException
 
 class SplitScreenUtils {
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 431cfbe..ae6757f 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -59,6 +59,7 @@
 import android.util.Log;
 import android.util.Pair;
 import android.view.SurfaceControl;
+import android.view.View;
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.RemoteTransition;
 import android.window.RemoteTransitionStub;
@@ -148,9 +149,10 @@
 
     /**
      * Should be a constant from {@link com.android.internal.jank.Cuj} or -1, does not need to be
-     * set for all launches.
+     * set for all launches. Used in conjunction with {@link #mLaunchingViewCuj} below.
      */
     private int mLaunchCuj = -1;
+    private View mLaunchingViewCuj;
 
     private FloatingTaskView mFirstFloatingTaskView;
     private SplitInstructionsView mSplitInstructionsView;
@@ -650,7 +652,12 @@
         return mSplitAnimationController;
     }
 
-    public void setLaunchingCuj(int launchCuj) {
+    /**
+     * Set params to invoke a trace session for the given view and CUJ when we begin animating the
+     * split launch AFTER we get a response from Shell.
+     */
+    public void setLaunchingCuj(View launchingView, int launchCuj) {
+        mLaunchingViewCuj = launchingView;
         mLaunchCuj = launchCuj;
     }
 
@@ -688,6 +695,9 @@
                         && mLaunchingTaskView.getRecentsView() != null
                         && mLaunchingTaskView.getRecentsView().isTaskViewVisible(
                         mLaunchingTaskView);
+                if (mLaunchingViewCuj != null && mLaunchCuj != -1) {
+                    InteractionJankMonitorWrapper.begin(mLaunchingViewCuj, mLaunchCuj);
+                }
                 mSplitAnimationController.playSplitLaunchAnimation(
                         shouldLaunchFromTaskView ? mLaunchingTaskView : null,
                         mLaunchingIconView,
@@ -750,6 +760,7 @@
             InteractionJankMonitorWrapper.end(mLaunchCuj);
         }
         mLaunchCuj = -1;
+        mLaunchingViewCuj = null;
 
         if (mSessionInstanceIds != null) {
             mStatsLogManager.logger()
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 56e91ed..828322b 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -39,7 +39,7 @@
 import com.android.quickstep.TaskAnimationManager;
 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-import com.android.wm.shell.pip.PipContentOverlay;
+import com.android.wm.shell.shared.pip.PipContentOverlay;
 
 /**
  * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 8e48f22..41add54 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.views
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.Point
 import android.graphics.PointF
@@ -23,9 +24,9 @@
 import android.graphics.drawable.shapes.RoundRectShape
 import android.util.AttributeSet
 import android.util.Log
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
-import android.view.ViewGroup
 import androidx.core.content.res.ResourcesCompat
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
@@ -125,11 +126,7 @@
                     snapshotView,
                     // Add snapshotView to the front after initial views e.g. icon and
                     // background.
-                    childCountAtInflation,
-                    LayoutParams(
-                        ViewGroup.LayoutParams.WRAP_CONTENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT
-                    )
+                    childCountAtInflation
                 )
                 TaskContainer(
                     this,
@@ -158,28 +155,37 @@
         }
     }
 
-    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-        val containerWidth = MeasureSpec.getSize(widthMeasureSpec)
-        var containerHeight = MeasureSpec.getSize(heightMeasureSpec)
-        setMeasuredDimension(containerWidth, containerHeight)
-
+    @SuppressLint("RtlHardcoded")
+    override fun updateTaskSize(
+        lastComputedTaskSize: Rect,
+        lastComputedGridTaskSize: Rect,
+        lastComputedCarouselTaskSize: Rect
+    ) {
+        super.updateTaskSize(
+            lastComputedTaskSize,
+            lastComputedGridTaskSize,
+            lastComputedCarouselTaskSize
+        )
         if (taskContainers.isEmpty()) {
             return
         }
 
         val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
-        containerHeight -= thumbnailTopMarginPx
+
+        val containerWidth = layoutParams.width
+        val containerHeight = layoutParams.height - thumbnailTopMarginPx
 
         BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+
         val windowWidth = tempPointF.x.toInt()
         val windowHeight = tempPointF.y.toInt()
         val scaleWidth = containerWidth / windowWidth.toFloat()
         val scaleHeight = containerHeight / windowHeight.toFloat()
+
         if (DEBUG) {
             Log.d(
                 TAG,
-                "onMeasure: container=[$containerWidth,$containerHeight] " +
+                "onMeasure: container=[$containerWidth,$containerHeight]" +
                     "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]"
             )
         }
@@ -195,27 +201,26 @@
                         right = windowWidth / 4
                         bottom = windowHeight / 4
                     }
-            val thumbWidth = (taskSize.width() * scaleWidth).toInt()
-            val thumbHeight = (taskSize.height() * scaleHeight).toInt()
-            it.snapshotView.measure(
-                MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY)
-            )
+            val positionInParent = it.task.positionInParent ?: ORIGIN
 
             // Position the task to the same position as it would be on the desktop
-            val positionInParent = it.task.positionInParent ?: ORIGIN
-            val taskX = (positionInParent.x * scaleWidth).toInt()
-            var taskY = (positionInParent.y * scaleHeight).toInt()
-            // move task down by margin size
-            taskY += thumbnailTopMarginPx
-            it.snapshotView.x = taskX.toFloat()
-            it.snapshotView.y = taskY.toFloat()
+            it.snapshotView.updateLayoutParams<LayoutParams> {
+                gravity = Gravity.LEFT or Gravity.TOP
+                width = (taskSize.width() * scaleWidth).toInt()
+                height = (taskSize.height() * scaleHeight).toInt()
+                leftMargin = (positionInParent.x * scaleWidth).toInt()
+                topMargin =
+                    (positionInParent.y * scaleHeight).toInt() +
+                        container.deviceProfile.overviewTaskThumbnailTopMarginPx
+            }
             if (DEBUG) {
-                Log.d(
-                    TAG,
-                    "onMeasure: task=${it.task.key} thumb=[$thumbWidth,$thumbHeight]" +
-                        " pos=[$taskX,$taskY]"
-                )
+                with(it.snapshotView.layoutParams as LayoutParams) {
+                    Log.d(
+                        TAG,
+                        "onMeasure: task=${it.task.key} size=[$width,$height]" +
+                            " margin=[$leftMargin,$topMargin]"
+                    )
+                }
             }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 9f268a0..8b87718 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -191,7 +191,7 @@
     private @SplitBannerConfig int getSplitBannerConfig() {
         if (mSplitBounds == null
                 || !mContainer.getDeviceProfile().isTablet
-                || mTaskView.isFocusedTask()) {
+                || mTaskView.isLargeTile()) {
             return SPLIT_BANNER_FULLSCREEN;
         }
 
diff --git a/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt
new file mode 100644
index 0000000..c893016
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.ViewGroup
+import android.widget.ImageView
+
+/**
+ * An [ImageView] that does not requestLayout() unless setLayoutParams is called.
+ *
+ * This is useful, particularly during animations, for [ImageView]s that are not supposed to be
+ * resized.
+ */
+@SuppressLint("AppCompatCustomView")
+class FixedSizeImageView : ImageView {
+    private var shouldRequestLayoutOnChanges = false
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+    ) : super(context, attrs, defStyleAttr)
+
+    override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
+        shouldRequestLayoutOnChanges = true
+        super.setLayoutParams(params)
+        shouldRequestLayoutOnChanges = false
+    }
+
+    override fun requestLayout() {
+        if (shouldRequestLayoutOnChanges) {
+            super.requestLayout()
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index d9468c7..4a2be2a 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -274,7 +274,9 @@
     }
 
     private void updateActionButtonsVisibility() {
-        assert mDp != null;
+        if (mDp == null) {
+            return;
+        }
         boolean showSingleTaskActions = !mIsGroupedTask;
         boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair;
         Log.d(TAG, "updateActionButtonsVisibility() called: showSingleTaskActions = ["
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index bb2a12f..f1ff026 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -37,6 +37,7 @@
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
@@ -211,6 +212,7 @@
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsAtomicAnimationFactory;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.RecentsViewUtils;
 import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps;
 import com.android.quickstep.util.SplitAnimationTimings;
 import com.android.quickstep.util.SplitSelectStateController;
@@ -229,9 +231,9 @@
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
 
 import kotlin.Unit;
 
@@ -254,8 +256,8 @@
  * @param <CONTAINER_TYPE> : the container that should host recents view
  * @param <STATE_TYPE>     : the type of base state that will be used
  */
-
-public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewContainer,
+public abstract class RecentsView<
+        CONTAINER_TYPE extends Context & RecentsViewContainer,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
         TaskVisualsChangeListener {
@@ -656,7 +658,7 @@
     protected boolean mRunningTaskTileHidden;
     @Nullable
     private Task[] mTmpRunningTasks;
-    protected int mFocusedTaskViewId = -1;
+    protected int mFocusedTaskViewId = INVALID_TASK_ID;
 
     private boolean mTaskIconScaledDown = false;
     private boolean mRunningTaskShowScreenshot = false;
@@ -813,7 +815,9 @@
     private boolean mAnyTaskHasBeenDismissed;
 
     private final RecentsViewModel mRecentsViewModel;
-    private final RecentsViewHelper mHelper;
+    private final RecentsViewModelHelper mHelper;
+
+    private final RecentsViewUtils mRecentsViewUtils = new RecentsViewUtils();
 
     public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             BaseContainerInterface sizeStrategy) {
@@ -835,7 +839,7 @@
                     recentsDependencies.inject(RecentTasksRepository.class),
                     recentsDependencies.inject(RecentsViewData.class)
             );
-            mHelper = new RecentsViewHelper(mRecentsViewModel);
+            mHelper = new RecentsViewModelHelper(mRecentsViewModel);
 
             recentsDependencies.provide(RecentsRotationStateRepository.class,
                     () -> new RecentsRotationStateRepositoryImpl(mOrientationState));
@@ -1685,10 +1689,10 @@
                 }
                 TaskView taskView = getTaskViewAt(mNextPage);
                 // Snap to fully visible focused task and clear all button.
-                boolean shouldSnapToFocusedTask = taskView != null && taskView.isFocusedTask()
+                boolean shouldSnapToLargeTask = taskView != null && taskView.isLargeTile()
                         && isTaskViewFullyVisible(taskView);
                 boolean shouldSnapToClearAll = mNextPage == indexOfChild(mClearAllButton);
-                if (!shouldSnapToFocusedTask && !shouldSnapToClearAll) {
+                if (!shouldSnapToLargeTask && !shouldSnapToClearAll) {
                     return;
                 }
             }
@@ -1748,7 +1752,9 @@
             return;
         }
 
-        if (mCurrentPage == 0) {
+        int frontIndex = enableLargeDesktopWindowingTile() ? getDesktopTaskViewCount() : 0;
+
+        if (mCurrentPage <= frontIndex) {
             return;
         }
 
@@ -1760,8 +1766,9 @@
         removeView(runningTaskView);
         mMovingTaskView = null;
         runningTaskView.resetPersistentViewTransforms();
-        addView(runningTaskView, 0);
-        setCurrentPage(0);
+
+        addView(runningTaskView, frontIndex);
+        setCurrentPage(frontIndex);
 
         updateTaskSize();
     }
@@ -1779,7 +1786,8 @@
 
     protected void applyLoadPlan(List<GroupTask> taskGroups) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups));
+            final List<GroupTask> finalTaskGroups = taskGroups;
+            mPendingAnimation.addEndListener(success -> applyLoadPlan(finalTaskGroups));
             return;
         }
 
@@ -1824,12 +1832,15 @@
 
         // Reset the focused task to avoiding initializing TaskViews layout as focused task during
         // binding. The focused task view will be updated after all the TaskViews are bound.
-        mFocusedTaskViewId = INVALID_TASK_ID;
+        setFocusedTaskViewId(INVALID_TASK_ID);
 
         // Removing views sets the currentPage to 0, so we save this and restore it after
         // the new set of views are added
         int previousCurrentPage = mCurrentPage;
         int previousFocusedPage = indexOfChild(getFocusedChild());
+        // TaskIds will no longer be valid after remove and re-add, clearing mTopRowIdSet.
+        mAnyTaskHasBeenDismissed = false;
+        mTopRowIdSet.clear();
         removeAllViews();
 
         // If we are entering Overview as a result of initiating a split from somewhere else
@@ -1847,6 +1858,11 @@
         // Clear out desktop view if it is set
         mDesktopTaskView = null;
 
+        // Move Desktop Tasks to the end of the list
+        if (enableLargeDesktopWindowingTile()) {
+            taskGroups = mRecentsViewUtils.sortDesktopTasksToFront(taskGroups);
+        }
+
         // Add views as children based on whether it's grouped or single task. Looping through
         // taskGroups backwards populates the thumbnail grid from least recent to most recent.
         for (int i = taskGroups.size() - 1; i >= 0; i--) {
@@ -1900,11 +1916,14 @@
         // Keep same previous focused task
         TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
         // If the list changed, maybe the focused task doesn't exist anymore
-        if (newFocusedTaskView == null && getTaskViewCount() > 0) {
-            newFocusedTaskView = getTaskViewAt(0);
+        int newFocusedTaskViewIndex = mRecentsViewUtils.getFocusedTaskIndex(taskGroups);
+        if (newFocusedTaskView == null && getTaskViewCount() > newFocusedTaskViewIndex) {
+            newFocusedTaskView = getTaskViewAt(newFocusedTaskViewIndex);
         }
-        mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview()
-                ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID;
+
+        setFocusedTaskViewId(newFocusedTaskView != null && !enableGridOnlyOverview()
+                ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID);
+
         updateTaskSize();
         updateChildTaskOrientations();
 
@@ -1944,8 +1963,8 @@
             // Set the current page to the running task, but not if settling on new task.
             if (hasAllValidTaskIds(runningTaskIds)) {
                 targetPage = indexOfChild(newRunningTaskView);
-            } else if (getTaskViewCount() > 0) {
-                targetPage = indexOfChild(requireTaskViewAt(0));
+            } else if (getTaskViewCount() > newFocusedTaskViewIndex) {
+                targetPage = indexOfChild(requireTaskViewAt(newFocusedTaskViewIndex));
             }
         }
         if (targetPage != -1 && mCurrentPage != targetPage) {
@@ -1970,6 +1989,7 @@
             // generally map to the same task.
             mIgnoreResetTaskId = INVALID_TASK_ID;
         }
+
         resetTaskVisuals();
         onTaskStackUpdated();
         updateEnabledOverlays();
@@ -2010,14 +2030,13 @@
         return taskViewCount;
     }
 
-    public int getGroupedTaskViewCount() {
-        int groupViewCount = 0;
-        for (int i = 0; i < getChildCount(); i++) {
-            if (getChildAt(i) instanceof GroupedTaskView) {
-                groupViewCount++;
-            }
-        }
-        return groupViewCount;
+    /**
+     * Transverse RecentsView children to calculate the amount of DesktopTaskViews.
+     *
+     * @return Number of children that are instances of DesktopTaskView
+     */
+    private int getDesktopTaskViewCount() {
+        return mRecentsViewUtils.getDesktopTaskViewCount(getChildCount(), this::getTaskViewAt);
     }
 
     /**
@@ -2549,7 +2568,7 @@
         mCurrentPageScrollDiff = 0;
         mIgnoreResetTaskId = -1;
         mTaskListChangeId = -1;
-        mFocusedTaskViewId = -1;
+        setFocusedTaskViewId(INVALID_TASK_ID);
         mAnyTaskHasBeenDismissed = false;
 
 
@@ -2621,6 +2640,10 @@
         return getTaskViewFromTaskViewId(mFocusedTaskViewId);
     }
 
+    private @Nullable TaskView getFirstLargeTaskView() {
+        return mRecentsViewUtils.getFirstLargeTaskView(getChildCount(), this::getTaskViewAt);
+    }
+
     @Nullable
     private TaskView getTaskViewFromTaskViewId(int taskViewId) {
         if (taskViewId == -1) {
@@ -2882,6 +2905,7 @@
         if (runningTasks.length == 0) {
             return;
         }
+
         int runningTaskViewId = -1;
         boolean needGroupTaskView = runningTasks.length > 1;
         boolean needDesktopTask = hasDesktopTask(runningTasks);
@@ -2926,7 +2950,11 @@
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
         setCurrentTask(runningTaskViewId);
-        mFocusedTaskViewId = enableGridOnlyOverview() ? INVALID_TASK_ID : runningTaskViewId;
+
+        boolean shouldFocusRunningTask = !(enableGridOnlyOverview()
+                || (enableLargeDesktopWindowingTile()
+                && getRunningTaskView() instanceof DesktopTaskView));
+        setFocusedTaskViewId(shouldFocusRunningTask ? runningTaskViewId : INVALID_TASK_ID);
         runOnPageScrollsInitialized(() -> setCurrentPage(getRunningTaskIndex()));
         setRunningTaskViewShowScreenshot(false);
         setRunningTaskHidden(runningTaskTileHidden);
@@ -2978,6 +3006,10 @@
         }
     }
 
+    private void setFocusedTaskViewId(int viewId) {
+        mFocusedTaskViewId = viewId;
+    }
+
     private int getTaskViewIdFromTaskId(int taskId) {
         TaskView taskView = getTaskViewByTaskId(taskId);
         return taskView != null ? taskView.getTaskViewId() : -1;
@@ -3003,6 +3035,10 @@
         TaskView runningTaskView = getRunningTaskView();
         if (runningTaskView != null) {
             runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot);
+            if (!enableRefactorTaskThumbnail()) {
+                runningTaskView.getTaskContainers().forEach(
+                        taskContainer -> taskContainer.getThumbnailViewDeprecated().refresh());
+            }
         }
         if (enableRefactorTaskThumbnail()) {
             mRecentsViewModel.setRunningTaskShowScreenshot(showScreenshot);
@@ -3077,8 +3113,9 @@
         float[] gridTranslations = new float[taskCount];
 
         int focusedTaskIndex = Integer.MAX_VALUE;
+        Set<Integer> largeTasksIndices = new HashSet<>();
         int focusedTaskShift = 0;
-        int focusedTaskWidthAndSpacing = 0;
+        int largeTaskWidthAndSpacing = 0;
         int snappedTaskRowWidth = 0;
         int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
         TaskView snappedTaskView = getTaskViewAt(snappedPage);
@@ -3095,12 +3132,11 @@
             // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
             // which case keep tasks in their respective rows. For the running task, don't join
             // the grid.
-            if (taskView.isFocusedTask()) {
+            boolean isLargeTile = taskView.isLargeTile();
+
+            if (isLargeTile) {
                 topRowWidth += taskWidthAndSpacing;
                 bottomRowWidth += taskWidthAndSpacing;
-
-                focusedTaskIndex = i;
-                focusedTaskWidthAndSpacing = taskWidthAndSpacing;
                 gridTranslations[i] += focusedTaskShift;
                 gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
 
@@ -3108,6 +3144,12 @@
                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
                         - taskView.getLayoutParams().height) / 2f);
 
+                if (taskView.getTaskViewId() == mFocusedTaskViewId) {
+                    focusedTaskIndex = i;
+                }
+                largeTasksIndices.add(i);
+                largeTaskWidthAndSpacing = taskWidthAndSpacing;
+
                 if (taskView == snappedTaskView) {
                     // If focused task is snapped, the row width is just task width and spacing.
                     snappedTaskRowWidth = taskWidthAndSpacing;
@@ -3116,7 +3158,7 @@
                 if (i > focusedTaskIndex) {
                     // For tasks after the focused task, shift by focused task's width and spacing.
                     gridTranslations[i] +=
-                            mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+                            mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
                 } else {
                     // For task before the focused task, accumulate the width and spacing to
                     // calculate the distance focused task need to shift.
@@ -3152,7 +3194,7 @@
                     // Move horizontally into empty space.
                     float widthOffset = 0;
                     for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
-                        if (j == focusedTaskIndex) {
+                        if (largeTasksIndices.contains(j)) {
                             continue;
                         }
                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
@@ -3171,7 +3213,7 @@
                     // Move horizontally into empty space.
                     float widthOffset = 0;
                     for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
-                        if (j == focusedTaskIndex) {
+                        if (largeTasksIndices.contains(j)) {
                             continue;
                         }
                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
@@ -3221,12 +3263,22 @@
         // accordingly. Update longRowWidth if ClearAllButton has been moved.
         float clearAllShortTotalWidthTranslation = 0;
         int longRowWidth = Math.max(topRowWidth, bottomRowWidth);
-        if (longRowWidth < mLastComputedGridSize.width()) {
-            mClearAllShortTotalWidthTranslation =
-                    (mIsRtl
-                            ? mLastComputedTaskSize.right
-                            : deviceProfile.widthPx - mLastComputedTaskSize.left)
-                            - longRowWidth - deviceProfile.overviewGridSideMargin;
+
+        // If Recents contains only large task sizes, it should only consider 1 large size
+        // for ClearAllButton translation. The space at the left side of the large task will be
+        // empty and it should be move ClearAllButton further away as well.
+        // TODO(b/359573248): Validate the translation for ClearAllButton for grid only.
+        boolean hasOnlyLargeTasks = taskCount == largeTasksIndices.size();
+        if (enableLargeDesktopWindowingTile() && hasOnlyLargeTasks) {
+            longRowWidth = largeTaskWidthAndSpacing;
+        }
+
+        // If first task is not in the expected position (mLastComputedTaskSize) and being too close
+        // to ClearAllButton, then apply extra translation to ClearAllButton.
+        int firstTaskStart = mLastComputedGridSize.left + longRowWidth;
+        int expectedFirstTaskStart = mLastComputedTaskSize.right;
+        if (firstTaskStart < expectedFirstTaskStart) {
+            mClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
             clearAllShortTotalWidthTranslation = mIsRtl
                     ? -mClearAllShortTotalWidthTranslation : mClearAllShortTotalWidthTranslation;
             if (snappedTaskRowWidth == longRowWidth) {
@@ -3241,10 +3293,10 @@
         float clearAllTotalTranslationX =
                 clearAllAccumulatedTranslation + clearAllShorterRowCompensation
                         + clearAllShortTotalWidthTranslation + snappedTaskNonGridScrollAdjustment;
-        if (focusedTaskIndex < taskCount) {
+        if (!largeTasksIndices.isEmpty()) {
             // Shift by focused task's width and spacing if a task is focused.
             clearAllTotalTranslationX +=
-                    mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+                    mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
         }
 
         // Make sure there are enough space between snapped page and ClearAllButton, for the case
@@ -3284,7 +3336,6 @@
         mClearAllButton.setGridScrollOffset(
                 mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left
                         : mLastComputedTaskSize.right - mLastComputedGridSize.right);
-
         setGridProgress(mGridProgress);
     }
 
@@ -3292,11 +3343,11 @@
         if (taskView1 == null || taskView2 == null) {
             return false;
         }
-        int taskViewId1 = taskView1.getTaskViewId();
-        int taskViewId2 = taskView2.getTaskViewId();
-        if (taskViewId1 == mFocusedTaskViewId || taskViewId2 == mFocusedTaskViewId) {
+        if (taskView1.isLargeTile() || taskView2.isLargeTile()) {
             return false;
         }
+        int taskViewId1 = taskView1.getTaskViewId();
+        int taskViewId2 = taskView2.getTaskViewId();
         return (mTopRowIdSet.contains(taskViewId1) && mTopRowIdSet.contains(taskViewId2)) || (
                 !mTopRowIdSet.contains(taskViewId1) && !mTopRowIdSet.contains(taskViewId2));
     }
@@ -3549,11 +3600,11 @@
                     isStagingFocusedTask = true;
                 } else {
                     nextFocusedTaskFromTop =
-                            mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
+                            !mTopRowIdSet.isEmpty() && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
                     // Pick the next focused task from the preferred row.
                     for (int i = 0; i < taskCount; i++) {
                         TaskView taskView = requireTaskViewAt(i);
-                        if (taskView == dismissedTaskView) {
+                        if (taskView == dismissedTaskView || taskView.isLargeTile()) {
                             continue;
                         }
                         boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
@@ -4017,9 +4068,9 @@
                     } else {
                         // Update focus task and its size.
                         if (finalIsFocusedTaskDismissed && finalNextFocusedTaskView != null) {
-                            mFocusedTaskViewId = enableGridOnlyOverview()
+                            setFocusedTaskViewId(enableGridOnlyOverview()
                                     ? INVALID_TASK_ID
-                                    : finalNextFocusedTaskView.getTaskViewId();
+                                    : finalNextFocusedTaskView.getTaskViewId());
                             mTopRowIdSet.remove(mFocusedTaskViewId);
                             finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
                         }
@@ -4162,8 +4213,9 @@
         IntArray bottomArray = new IntArray(bottomRowIdArraySize);
         int taskViewCount = getTaskViewCount();
         for (int i = 0; i < taskViewCount; i++) {
-            int taskViewId = requireTaskViewAt(i).getTaskViewId();
-            if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
+            TaskView taskView = requireTaskViewAt(i);
+            int taskViewId = taskView.getTaskViewId();
+            if (!mTopRowIdSet.contains(taskViewId) && !taskView.isLargeTile()) {
                 bottomArray.add(taskViewId);
             }
         }
@@ -4269,6 +4321,7 @@
         }
 
         // Init task grid nav helper with top/bottom id arrays.
+        // TODO(b/361070854): Add keyboard navigation for all large tiles.
         TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
                 getBottomRowIdArray(), mFocusedTaskViewId);
 
@@ -4597,9 +4650,10 @@
                 ? (runningTask == null ? INVALID_PAGE : indexOfChild(runningTask))
                 : mOffsetMidpointIndexOverride;
         int modalMidpoint = getCurrentPage();
-        boolean isModalGridWithoutFocusedTask =
-                showAsGrid && enableGridOnlyOverview() && mTaskModalness > 0;
-        if (isModalGridWithoutFocusedTask) {
+        boolean shouldCalculateOffsetForAllTasks = showAsGrid
+                && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile())
+                && mTaskModalness > 0;
+        if (shouldCalculateOffsetForAllTasks) {
             modalMidpoint = indexOfChild(mSelectedTask);
         }
 
@@ -4638,7 +4692,7 @@
                     : i < midpoint
                             ? leftOffsetSize
                             : rightOffsetSize;
-            if (isModalGridWithoutFocusedTask) {
+            if (shouldCalculateOffsetForAllTasks) {
                 gridOffsetSize = getHorizontalOffsetSize(i, modalMidpoint, modalOffset);
                 gridOffsetSize = Math.abs(gridOffsetSize) * (i <= modalMidpoint ? 1 : -1);
             }
@@ -5244,10 +5298,7 @@
 
         float toScale = getMaxScaleForFullScreen();
         boolean showAsGrid = showAsGrid();
-        boolean zoomInTaskView = showAsGrid
-                ? ((taskView.isFocusedTask() && isTaskViewFullyVisible(taskView))
-                || taskView instanceof DesktopTaskView)
-                : taskIndex == centerTaskIndex;
+        boolean zoomInTaskView = showAsGrid ? taskView.isLargeTile() : taskIndex == centerTaskIndex;
         if (zoomInTaskView) {
             anim.play(ObjectAnimator.ofFloat(this, RECENTS_SCALE_PROPERTY, toScale));
             anim.play(ObjectAnimator.ofFloat(this, FULLSCREEN_PROGRESS, 1));
@@ -5728,8 +5779,8 @@
     }
 
     private int getFirstViewIndex() {
-        TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null;
-        return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0;
+        TaskView firstTaskView = mShowAsGridLastOnLayout ? getFirstLargeTaskView() : null;
+        return firstTaskView != null ? indexOfChild(firstTaskView) : 0;
     }
 
     private int getLastViewIndex() {
@@ -5747,7 +5798,7 @@
         }
 
         // Returns focus task if there are no grid tasks.
-        return indexOfChild(getFocusedTaskView());
+        return indexOfChild(getFirstLargeTaskView());
     }
 
     /**
@@ -5963,7 +6014,7 @@
     public boolean isOnGridBottomRow(TaskView taskView) {
         return showAsGrid()
                 && !mTopRowIdSet.contains(taskView.getTaskViewId())
-                && taskView.getTaskViewId() != mFocusedTaskViewId;
+                && !taskView.isLargeTile();
     }
 
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
similarity index 91%
rename from quickstep/src/com/android/quickstep/views/RecentsViewHelper.kt
rename to quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index e8c9718..5b71da1 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -27,8 +27,8 @@
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.launch
 
-/** Helper for [RecentsView] to interact with coroutine. */
-class RecentsViewHelper(private val recentsViewModel: RecentsViewModel) {
+/** Helper for [RecentsView] to interact with the [RecentsViewModel]. */
+class RecentsViewModelHelper(private val recentsViewModel: RecentsViewModel) {
     private lateinit var viewAttachedScope: CoroutineScope
 
     fun onAttachedToWindow() {
@@ -43,7 +43,7 @@
     fun switchToScreenshot(
         taskView: TaskView,
         recentsAnimationController: RecentsAnimationController,
-        onFinishRunnable: Runnable
+        onFinishRunnable: Runnable,
     ) {
         val updatedThumbnails =
             taskView.taskContainers.associate {
@@ -55,7 +55,7 @@
     fun switchToScreenshot(
         taskView: TaskView,
         updatedThumbnails: Map<Int, ThumbnailData>?,
-        onFinishRunnable: Runnable
+        onFinishRunnable: Runnable,
     ) {
         // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside
         // viewAttachedScope.
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index 158ae33..19d706f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -99,7 +99,7 @@
     private var optionMeasuredHeight = 0
     private val arrowHorizontalPadding: Int
         get() =
-            if (taskView.isFocusedTask)
+            if (taskView.isLargeTile)
                 resources.getDimensionPixelSize(R.dimen.task_menu_horizontal_padding)
             else 0
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 176a538..e43d7b4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -49,6 +49,7 @@
 import com.android.launcher3.Flags.enableFocusOutline
 import com.android.launcher3.Flags.enableGridOnlyOverview
 import com.android.launcher3.Flags.enableHoverOfChildElementsInTaskview
+import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
@@ -108,7 +109,7 @@
     defStyleRes: Int = 0,
     focusBorderAnimator: BorderAnimator? = null,
     hoverBorderAnimator: BorderAnimator? = null,
-    type: TaskViewType = TaskViewType.SINGLE
+    private val type: TaskViewType = TaskViewType.SINGLE,
 ) : FrameLayout(context, attrs), ViewPool.Reusable {
     /**
      * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which
@@ -133,13 +134,15 @@
 
     val isGridTask: Boolean
         /** Returns whether the task is part of overview grid and not being focused. */
-        get() = container.deviceProfile.isTablet && !isFocusedTask
+        get() = container.deviceProfile.isTablet && !isLargeTile
 
     val isRunningTask: Boolean
         get() = this === recentsView?.runningTaskView
 
-    val isFocusedTask: Boolean
-        get() = this === recentsView?.focusedTaskView
+    val isLargeTile: Boolean
+        get() =
+            this == recentsView?.focusedTaskView ||
+                (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP)
 
     val taskCornerRadius: Float
         get() = currentFullscreenParams.cornerRadius
@@ -290,9 +293,6 @@
         set(value) {
             field = value
             applyScale()
-            if (enableRefactorTaskThumbnail()) {
-                taskViewModel.updateNonGridScale(value)
-            }
         }
 
     private var dismissScale = 1f
@@ -521,7 +521,7 @@
     public override fun onFocusChanged(
         gainFocus: Boolean,
         direction: Int,
-        previouslyFocusedRect: Rect?
+        previouslyFocusedRect: Rect?,
     ) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
         if (borderEnabled) {
@@ -682,7 +682,7 @@
     open fun bind(
         task: Task,
         orientedState: RecentsOrientedState,
-        taskOverlayFactory: TaskOverlayFactory
+        taskOverlayFactory: TaskOverlayFactory,
     ) {
 
         cancelPendingLoadTasks()
@@ -707,7 +707,7 @@
         @IdRes iconViewId: Int,
         @IdRes showWindowViewId: Int,
         @StagePosition stagePosition: Int,
-        taskOverlayFactory: TaskOverlayFactory
+        taskOverlayFactory: TaskOverlayFactory,
     ): TaskContainer {
         val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!!
         val snapshotView =
@@ -774,10 +774,10 @@
      * Updates TaskView scaling and translation required to support variable width if enabled, while
      * ensuring TaskView fits into screen in fullscreen.
      */
-    fun updateTaskSize(
+    open fun updateTaskSize(
         lastComputedTaskSize: Rect,
         lastComputedGridTaskSize: Rect,
-        lastComputedCarouselTaskSize: Rect
+        lastComputedCarouselTaskSize: Rect,
     ) {
         val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx
         val taskWidth = lastComputedTaskSize.width()
@@ -789,9 +789,10 @@
         if (container.deviceProfile.isTablet) {
             val boxWidth: Int
             val boxHeight: Int
-            if (isFocusedTask) {
-                // Task will be focused and should use focused task size. Use focusTaskRatio
-                // that is associated with the original orientation of the focused task.
+
+            // Focused task and Desktop tasks should use focusTaskRatio that is associated
+            // with the original orientation of the focused task.
+            if (isLargeTile) {
                 boxWidth = taskWidth
                 boxHeight = taskHeight
             } else {
@@ -1334,7 +1335,7 @@
     private fun computeAndSetIconTouchDelegate(
         view: TaskViewIcon,
         tempCenterCoordinates: FloatArray,
-        transformingTouchDelegate: TransformingTouchDelegate
+        transformingTouchDelegate: TransformingTouchDelegate,
     ) {
         val viewHalfWidth = view.width / 2f
         val viewHalfHeight = view.height / 2f
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt
new file mode 100644
index 0000000..4aac1dc
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.view.View
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.TaskbarViewController.DIVIDER_VIEW_POSITION_OFFSET
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+/**
+ * Legend for the comments below:
+ * A: All Apps Button
+ * H: Hotseat item
+ * |: Divider
+ * R: Recent item
+ *
+ * The comments are formatted in two lines:
+ * // Items in taskbar, e.g.               A  |  HHHHHH
+ * // Index of items relative to Hotseat: -1 -.5 012345
+ */
+class TaskbarViewControllerTest {
+
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+    @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var taskbarViewController: TaskbarViewController
+
+    @Test
+    fun testGetPositionInHotseat_allAppsButton_nonRtl() {
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ 6,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ true,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1
+            )
+        // [>A<] | [HHHHHH]
+        //  -1 -.5  012345
+        assertThat(position).isEqualTo(-1)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_allAppsButton_rtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ true,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1
+            )
+        // [HHHHHH] | [>A<]
+        //  012345 5.5  6
+        assertThat(position).isEqualTo(numShownHotseatIcons)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_notForRecents_nonRtl() {
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ 6,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1
+            )
+        // [A] >|< [HHHHHH]
+        // -1  -.5  012345
+        assertThat(position).isEqualTo(-DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_forRecents_nonRtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ true,
+                /* recentTaskIndex = */ -1
+            )
+        // [A] [HHHHHH] >|< [RR]
+        // -1   012345  5.5  67
+        assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_notForRecents_rtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ -1
+            )
+        // [HHHHHH] >|< [A]
+        //  012345  5.5  6
+        assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_dividerView_forRecents_rtl() {
+        val numShownHotseatIcons = 6
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ true,
+                /* isDividerForRecents = */ true,
+                /* recentTaskIndex = */ -1
+            )
+        // [HHHHHH][A] >|< [RR]
+        //  012345  6  6.5  78
+        assertThat(position).isEqualTo(numShownHotseatIcons + DIVIDER_VIEW_POSITION_OFFSET)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_firstRecentIndex_nonRtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 0
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex
+            )
+        // [A][HHHHHH] | [>R<R]
+        // -1  012345 5.5  6 7
+        assertThat(position).isEqualTo(numShownHotseatIcons + recentTaskIndex)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_secondRecentIndex_nonRtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 1
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ false,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex
+            )
+        // [A][HHHHHH] | [R>R<]
+        // -1  012345 5.5 6 7
+        assertThat(position).isEqualTo(numShownHotseatIcons + recentTaskIndex)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_firstRecentIndex_rtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 0
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex
+            )
+        // [HHHHHH][A] | [>R<R]
+        //  012345  6 6.5  7 8
+        assertThat(position).isEqualTo(numShownHotseatIcons + 1 + recentTaskIndex)
+    }
+
+    @Test
+    fun testGetPositionInHotseat_recentTasks_secondRecentIndex_rtl() {
+        val numShownHotseatIcons = 6
+        val recentTaskIndex = 1
+        val position =
+            taskbarViewController.getPositionInHotseat(
+                /* numShownHotseatIcons = */ numShownHotseatIcons,
+                /* child = */ View(context),
+                /* isRtl = */ true,
+                /* isAllAppsButton = */ false,
+                /* isTaskbarDividerView = */ false,
+                /* isDividerForRecents = */ false,
+                /* recentTaskIndex = */ recentTaskIndex
+            )
+        // [HHHHHH][A] | [R>R<]
+        //  012345  6 6.5 7 8
+        assertThat(position).isEqualTo(numShownHotseatIcons + 1 + recentTaskIndex)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 4da06e1..7928ce9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -64,6 +64,7 @@
     private lateinit var bubble: BubbleBarBubble
     private lateinit var bubbleBarView: BubbleBarView
     private lateinit var bubbleStashController: BubbleStashController
+    private val onExpandedNoOp = Runnable {}
 
     @Before
     fun setUp() {
@@ -81,7 +82,12 @@
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = false)
@@ -125,7 +131,12 @@
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = false)
@@ -168,7 +179,12 @@
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = false)
@@ -208,7 +224,12 @@
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = false)
@@ -249,7 +270,12 @@
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = false)
@@ -278,8 +304,15 @@
         val handleAnimator = PhysicsAnimator.getInstance(handle)
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = true)
@@ -303,6 +336,7 @@
         assertThat(animatorScheduler.delayedBlock).isNull()
 
         verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -314,8 +348,15 @@
         val handleAnimator = PhysicsAnimator.getInstance(handle)
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = false)
@@ -345,6 +386,7 @@
             .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
         verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isFalse()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -356,8 +398,15 @@
         val handleAnimator = PhysicsAnimator.getInstance(handle)
         whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleInForStashed(bubble, isExpanding = false)
@@ -384,6 +433,7 @@
             .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
         verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isFalse()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -400,7 +450,12 @@
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = true, isExpanding = false)
@@ -442,8 +497,15 @@
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = true, isExpanding = true)
@@ -459,6 +521,7 @@
 
         assertThat(animatorScheduler.delayedBlock).isNull()
         verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -471,7 +534,12 @@
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
@@ -502,8 +570,15 @@
         whenever(bubbleStashController.bubbleBarTranslationY)
             .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
@@ -533,6 +608,7 @@
         verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -542,8 +618,15 @@
         whenever(bubbleStashController.bubbleBarTranslationY)
             .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
@@ -566,6 +649,7 @@
 
         verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(animator.isAnimating).isFalse()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -578,7 +662,12 @@
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpandedNoOp,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
@@ -617,8 +706,15 @@
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleBarForCollapsed(bubble, isExpanding = true)
@@ -645,6 +741,7 @@
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(bubbleBarView.isExpanded).isTrue()
         verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -656,8 +753,15 @@
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
@@ -695,6 +799,7 @@
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(bubbleBarView.isExpanded).isTrue()
         verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     @Test
@@ -706,8 +811,15 @@
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
+        var notifiedExpanded = false
+        val onExpanded = Runnable { notifiedExpanded = true }
         val animator =
-            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                onExpanded,
+                animatorScheduler
+            )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
@@ -742,6 +854,7 @@
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(bubbleBarView.isExpanded).isTrue()
         verify(bubbleStashController).showBubbleBarImmediate()
+        assertThat(notifiedExpanded).isTrue()
     }
 
     private fun setUpBubbleBar() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
index cdfbd16..fc2f029 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
@@ -20,8 +20,6 @@
     private var recentsDeviceProfile =
         RecentsDeviceProfile(
             isLargeScreen = false,
-            widthPx = 1080,
-            heightPx = 1920,
         )
 
     override fun getRecentsDeviceProfile() = recentsDeviceProfile
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
index e74fe4b..abe4142 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
@@ -39,6 +39,6 @@
         whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
 
         assertThat(systemUnderTest.getRecentsDeviceProfile())
-            .isEqualTo(RecentsDeviceProfile(isLargeScreen = true, widthPx = 1600, heightPx = 2560))
+            .isEqualTo(RecentsDeviceProfile(isLargeScreen = true))
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt
deleted file mode 100644
index 13e8b09..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.thumbnail
-
-import android.graphics.Point
-import android.graphics.drawable.Drawable
-import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
-import com.android.quickstep.task.viewmodel.TaskViewData
-import com.android.quickstep.views.TaskViewType
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-class GetSplashSizeUseCaseTest {
-    private val taskThumbnailViewData = TaskThumbnailViewData()
-    private val taskViewData = TaskViewData(TaskViewType.SINGLE)
-    private val recentsDeviceProfileRepository = FakeRecentsDeviceProfileRepository()
-    private val systemUnderTest =
-        GetSplashSizeUseCase(taskThumbnailViewData, taskViewData, recentsDeviceProfileRepository)
-
-    @Test
-    fun execute_whenNoScaleRequired_returnsIntrinsicSize() {
-        taskThumbnailViewData.width.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx
-        taskThumbnailViewData.height.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx
-
-        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(100, 100))
-    }
-
-    @Test
-    fun execute_whenThumbnailViewIsSmallerThanScreen_returnsScaledSize() {
-        taskThumbnailViewData.width.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
-        taskThumbnailViewData.height.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
-
-        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(50, 50))
-    }
-
-    @Test
-    fun execute_whenThumbnailViewIsSmallerThanScreen_withNonGridScale_returnsScaledSize() {
-        taskThumbnailViewData.width.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
-        taskThumbnailViewData.height.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
-        taskViewData.nonGridScale.value = 2f
-
-        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(25, 25))
-    }
-
-    @Test
-    fun execute_whenThumbnailViewIsSmallerThanScreen_withThumbnailViewScale_returnsScaledSize() {
-        taskThumbnailViewData.width.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
-        taskThumbnailViewData.height.value =
-            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
-        taskThumbnailViewData.scaleX.value = 2f
-        taskThumbnailViewData.scaleY.value = 2f
-
-        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(25, 25))
-    }
-
-    private fun createIcon(width: Int, height: Int): Drawable =
-        mock<Drawable>().apply {
-            whenever(intrinsicWidth).thenReturn(width)
-            whenever(intrinsicHeight).thenReturn(height)
-        }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index fe7d37a..fcf4e56 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -21,7 +21,6 @@
 import android.graphics.Bitmap
 import android.graphics.Color
 import android.graphics.Matrix
-import android.graphics.Point
 import android.graphics.drawable.Drawable
 import android.view.Surface
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +34,6 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Splash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
@@ -46,10 +44,8 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -63,7 +59,6 @@
     private val tasksRepository = FakeTasksRepository()
     private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
     private val splashAlphaUseCase: SplashAlphaUseCase = mock()
-    private val getSplashSizeUseCase: GetSplashSizeUseCase = mock()
     private val systemUnderTest by lazy {
         TaskThumbnailViewModel(
             recentsViewData,
@@ -72,17 +67,11 @@
             tasksRepository,
             mGetThumbnailPositionUseCase,
             splashAlphaUseCase,
-            getSplashSizeUseCase,
         )
     }
 
     private val tasks = (0..5).map(::createTaskWithId)
 
-    @Before
-    fun setUp() {
-        whenever(getSplashSizeUseCase.execute(any())).thenReturn(Point())
-    }
-
     @Test
     fun initialStateIsUninitialized() = runTest {
         assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
@@ -120,7 +109,7 @@
                         bitmap = expectedThumbnailData.thumbnail!!,
                         thumbnailRotation = Surface.ROTATION_0,
                     ),
-                    Splash(expectedIconData.icon, Point())
+                    expectedIconData.icon
                 )
             )
     }
@@ -215,7 +204,7 @@
                         bitmap = expectedThumbnailData.thumbnail!!,
                         thumbnailRotation = Surface.ROTATION_270,
                     ),
-                    Splash(expectedIconData.icon, Point())
+                    expectedIconData.icon
                 )
             )
     }
@@ -241,29 +230,12 @@
                         bitmap = expectedThumbnailData.thumbnail!!,
                         thumbnailRotation = Surface.ROTATION_0,
                     ),
-                    Splash(expectedIconData.icon, Point())
+                    expectedIconData.icon
                 )
             )
     }
 
     @Test
-    fun bindStoppedTask_thenStateContainsSplashSizeFromUseCase() = runTest {
-        val taskId = 2
-        val expectedSplashSize = Point(100, 150)
-        whenever(getSplashSizeUseCase.execute(any())).thenReturn(expectedSplashSize)
-        val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
-        tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
-        val expectedIconData = createIconData("Task 2")
-        tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
-
-        systemUnderTest.bind(taskId)
-        val uiState = systemUnderTest.uiState.first() as SnapshotSplash
-        assertThat(uiState.splash.size).isEqualTo(expectedSplashSize)
-    }
-
-    @Test
     fun getSnapshotMatrix_MissingThumbnail() = runTest {
         val taskId = 2
         val isRtl = true
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index d9d5585..885a7f6 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -39,8 +39,8 @@
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.Task.TaskKey
 import com.android.window.flags.Flags
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index cbc8441..244b897 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -39,7 +39,7 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.wm.shell.util.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedRecentTaskInfo;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml
index be4765a..39af989 100644
--- a/res/drawable/add_item_dialog_background.xml
+++ b/res/drawable/add_item_dialog_background.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle" >
-    <solid android:color="?attr/materialColorSurfaceContainerHighest" />
+    <solid android:color="?attr/widgetPickerPrimarySurfaceColor" />
     <corners
         android:topLeftRadius="?android:attr/dialogCornerRadius"
         android:topRightRadius="?android:attr/dialogCornerRadius" />
diff --git a/res/drawable/bg_letter_list_text.xml b/res/drawable/bg_letter_list_text.xml
new file mode 100644
index 0000000..427702b
--- /dev/null
+++ b/res/drawable/bg_letter_list_text.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="?attr/materialColorSurfaceContainer" />
+    <corners android:radius="100dp"/>
+    <size
+        android:width="@dimen/bg_letter_list_text_size"
+        android:height="@dimen/bg_letter_list_text_size"/>
+</shape>
\ No newline at end of file
diff --git a/res/layout/all_apps_fast_scroller.xml b/res/layout/all_apps_fast_scroller.xml
index 0f1d933..7e16ca5 100644
--- a/res/layout/all_apps_fast_scroller.xml
+++ b/res/layout/all_apps_fast_scroller.xml
@@ -36,4 +36,17 @@
         android:layout_marginEnd="@dimen/fastscroll_end_margin"
         launcher:canThumbDetach="true" />
 
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/scroll_letter_layout"
+        android:layout_width="@dimen/fastscroll_width"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
+        android:layout_alignTop="@+id/all_apps_header"
+        android:layout_marginTop="@dimen/all_apps_header_bottom_padding"
+        android:layout_marginEnd="@dimen/fastscroll_list_letter_end_margin"
+        android:clipToPadding="false"
+        android:outlineProvider="none"
+        />
 </merge>
\ No newline at end of file
diff --git a/res/layout/fast_scroller_letter_list_text_view.xml b/res/layout/fast_scroller_letter_list_text_view.xml
new file mode 100644
index 0000000..493b6fc
--- /dev/null
+++ b/res/layout/fast_scroller_letter_list_text_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.launcher3.allapps.LetterListTextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/fastscroll_list_letter_size"
+    android:layout_height="@dimen/fastscroll_list_letter_size"
+    android:textSize="@dimen/fastscroll_list_letter_text_size"
+    android:importantForAccessibility="no"
+    android:gravity="center"
+    android:clickable="false">
+</com.android.launcher3.allapps.LetterListTextView>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 6453f79..5ba00c0 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -81,6 +81,11 @@
     <dimen name="fastscroll_popup_text_size">32dp</dimen>
     <dimen name="fastscroll_popup_margin">19dp</dimen>
 
+    <dimen name="fastscroll_list_letter_size">5dp</dimen>
+    <dimen name="fastscroll_list_letter_text_size">14sp</dimen>
+    <dimen name="fastscroll_list_letter_end_margin">-10dp</dimen>
+    <dimen name="bg_letter_list_text_size">20sp</dimen>
+
     <!--
       Fast scroller draws the content horizontally centered. The end of the track should be
       aligned at the end of the container.
@@ -456,6 +461,7 @@
     <!-- Overview placeholder to compile in Launcher3 without Quickstep -->
     <dimen name="task_thumbnail_icon_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size">0dp</dimen>
+    <dimen name="task_thumbnail_splash_icon_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size_grid">0dp</dimen>
     <dimen name="task_thumbnail_icon_menu_drawable_touch_size">0dp</dimen>
     <dimen name="task_menu_edge_padding">0dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 1eccbff..26e900d 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -176,7 +176,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mSkipUserBadge = false;
     @ViewDebug.ExportedProperty(category = "launcher")
-    private boolean mIsIconVisible = true;
+    protected boolean mIsIconVisible = true;
     @ViewDebug.ExportedProperty(category = "launcher")
     private int mTextColor;
     @ViewDebug.ExportedProperty(category = "launcher")
@@ -1043,12 +1043,11 @@
     /** Applies the given progress level to the this icon's progress bar. */
     @Nullable
     public PreloadIconDrawable applyProgressLevel() {
-        if (!(getTag() instanceof ItemInfoWithIcon)
+        if (!(getTag() instanceof ItemInfoWithIcon info)
                 || ((ItemInfoWithIcon) getTag()).isInactiveArchive()) {
             return null;
         }
 
-        ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
         int progressLevel = info.getProgressLevel();
         if (progressLevel >= 100) {
             setContentDescription(info.contentDescription != null
@@ -1068,6 +1067,10 @@
             } else {
                 preloadIconDrawable = makePreloadIcon();
                 setIcon(preloadIconDrawable);
+                if (info.isArchived() && Flags.useNewIconForArchivedApps()) {
+                    // reapply text without cloud icon as soon as unarchiving is triggered
+                    applyLabel(info);
+                }
             }
             return preloadIconDrawable;
         }
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index de1748b..6622e11 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -25,6 +25,7 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -54,9 +55,11 @@
         super(context, attrs, defStyleAttr);
     }
 
-    public void bindFastScrollbar(RecyclerViewFastScroller scrollbar) {
+    public void bindFastScrollbar(RecyclerViewFastScroller scrollbar,
+            RecyclerViewFastScroller.FastScrollerLocation location) {
         mScrollbar = scrollbar;
         mScrollbar.setRecyclerView(this);
+        mScrollbar.setFastScrollerLocation(location);
         onUpdateScrollbar(0);
     }
 
@@ -163,6 +166,13 @@
     public abstract void onUpdateScrollbar(int dy);
 
     /**
+     * Return the fast scroll letter list view in the A-Z list.
+     */
+    public ConstraintLayout getLetterList() {
+        return null;
+    }
+
+    /**
      * <p>Override in each subclass of this base class.
      */
     public void onFastScrollCompleted() {}
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 227ac2b..cc4724c 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -29,6 +29,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
+import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.ALL_APPS_SCROLLER;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -65,6 +66,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
 import androidx.annotation.VisibleForTesting;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.core.graphics.ColorUtils;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -168,6 +170,7 @@
     protected FloatingHeaderView mHeader;
     protected View mBottomSheetBackground;
     protected RecyclerViewFastScroller mFastScroller;
+    private ConstraintLayout mFastScrollLetterLayout;
 
     /**
      * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}.
@@ -282,6 +285,13 @@
         mSearchRecyclerView = findViewById(R.id.search_results_list_view);
         mFastScroller = findViewById(R.id.fast_scroller);
         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
+        mFastScrollLetterLayout = findViewById(R.id.scroll_letter_layout);
+        if (Flags.letterFastScroller()) {
+            // Set clip children to false otherwise the scroller letters will be clipped.
+            setClipChildren(false);
+        } else {
+            setClipChildren(true);
+        }
 
         mSearchContainer = inflateSearchBar();
         if (!isSearchBarFloating()) {
@@ -563,7 +573,8 @@
             mActivityContext.hideKeyboard();
         }
         if (mAH.get(currentActivePage).mRecyclerView != null) {
-            mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller);
+            mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller,
+                    ALL_APPS_SCROLLER);
         }
         // Header keeps track of active recycler view to properly render header protection.
         mHeader.setActiveRV(currentActivePage);
@@ -1500,6 +1511,10 @@
         }
     }
 
+    ConstraintLayout getFastScrollerLetterList() {
+        return mFastScrollLetterLayout;
+    }
+
     /**
      * redraws header protection
      */
@@ -1567,7 +1582,7 @@
         void setup(@NonNull View rv, @Nullable Predicate<ItemInfo> matcher) {
             mAppsList.updateItemFilter(matcher);
             mRecyclerView = (AllAppsRecyclerView) rv;
-            mRecyclerView.bindFastScrollbar(mFastScroller);
+            mRecyclerView.bindFastScrollbar(mFastScroller, ALL_APPS_SCROLLER);
             mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory());
             mRecyclerView.setApps(mAppsList);
             mRecyclerView.setLayoutManager(mLayoutManager);
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 2a47222..ae45a35 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,6 +15,9 @@
  */
 package com.android.launcher3.allapps;
 
+import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT;
+import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
+
 import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo;
@@ -36,22 +39,29 @@
 import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
 import androidx.core.util.Consumer;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.FastScrollRecyclerView;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.views.ActivityContext;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -66,6 +76,7 @@
     protected final int mNumAppsPerRow;
     private final AllAppsFastScrollHelper mFastScrollHelper;
     private int mCumulativeVerticalScroll;
+    private ConstraintLayout mLetterList;
 
     protected AlphabeticalAppsList<?> mApps;
 
@@ -238,6 +249,9 @@
             return;
         }
 
+        if (Flags.letterFastScroller() && !mScrollbar.isDraggingThumb()) {
+            setLettersToScrollLayout(mApps.getFastScrollerSections());
+        }
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
         int availableScrollHeight = getAvailableScrollHeight();
@@ -319,6 +333,80 @@
         return false;
     }
 
+    public void setLettersToScrollLayout(
+            List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections) {
+        if (mLetterList != null) {
+            mLetterList.removeAllViews();
+        }
+        Context context = getContext();
+        ActivityAllAppsContainerView<?> allAppsContainerView =
+                ActivityContext.lookupContext(context).getAppsView();
+        mLetterList = allAppsContainerView.getFastScrollerLetterList();
+        mLetterList.setPadding(0, getScrollBarTop(), 0, getScrollBarMarginBottom());
+        List<LetterListTextView> textViews = new ArrayList<>();
+        for (int i = 0; i < fastScrollSections.size(); i++) {
+            AlphabeticalAppsList.FastScrollSectionInfo sectionInfo = fastScrollSections.get(i);
+            LetterListTextView textView =
+                    (LetterListTextView) LayoutInflater.from(context).inflate(
+                            R.layout.fast_scroller_letter_list_text_view, mLetterList, false);
+            int viewId = View.generateViewId();
+            textView.setId(viewId);
+            sectionInfo.setId(viewId);
+            textView.setText(sectionInfo.sectionName);
+            if (i == fastScrollSections.size() - 1) {
+                // The last section info is just a duplicate so that user can scroll to the bottom.
+                textView.setVisibility(INVISIBLE);
+            }
+            ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
+                    MATCH_CONSTRAINT, WRAP_CONTENT);
+            lp.dimensionRatio = "v,1:1";
+            textView.setLayoutParams(lp);
+            textViews.add(textView);
+            mLetterList.addView(textView);
+        }
+        // Need to add an extra textview to be aligned.
+        LetterListTextView lastLetterListTextView = new LetterListTextView(context);
+        int currentId = View.generateViewId();
+        lastLetterListTextView.setId(currentId);
+        lastLetterListTextView.setVisibility(INVISIBLE);
+        textViews.add(lastLetterListTextView);
+        mLetterList.addView(lastLetterListTextView);
+        constraintTextViewsVertically(mLetterList, textViews);
+        mLetterList.setVisibility(VISIBLE);
+    }
+
+    private void constraintTextViewsVertically(ConstraintLayout constraintLayout,
+            List<LetterListTextView> textViews) {
+        ConstraintSet chain = new ConstraintSet();
+        chain.clone(constraintLayout);
+        for (int i = 0; i < textViews.size(); i++) {
+            LetterListTextView currentView = textViews.get(i);
+            if (i == 0) {
+                chain.connect(currentView.getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID,
+                        ConstraintSet.TOP);
+            } else {
+                chain.connect(currentView.getId(), ConstraintSet.TOP, textViews.get(i-1).getId(),
+                        ConstraintSet.BOTTOM);
+            }
+            chain.connect(currentView.getId(), ConstraintSet.START, constraintLayout.getId(),
+                    ConstraintSet.START);
+            chain.connect(currentView.getId(), ConstraintSet.END, constraintLayout.getId(),
+                    ConstraintSet.END);
+        }
+        int[] viewIds = textViews.stream().mapToInt(TextView::getId).toArray();
+        float[] weights = new float[textViews.size()];
+        Arrays.fill(weights,1); // fill with 1 for equal weights
+        chain.createVerticalChain(constraintLayout.getId(), ConstraintSet.TOP,
+                constraintLayout.getId(), ConstraintSet.BOTTOM, viewIds, weights,
+                ConstraintSet.CHAIN_SPREAD);
+        chain.applyTo(constraintLayout);
+    }
+
+    @Override
+    public ConstraintLayout getLetterList() {
+        return mLetterList;
+    }
+
     private void logCumulativeVerticalScroll() {
         ActivityContext context = ActivityContext.lookupContext(getContext());
         StatsLogManager mgr = context.getStatsLogManager();
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 6dd811a..8e44d65 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -74,11 +74,17 @@
         public final CharSequence sectionName;
         // The item position
         public final int position;
+        // The view id associated with this section
+        public int id = -1;
 
         public FastScrollSectionInfo(CharSequence sectionName, int position) {
             this.sectionName = sectionName;
             this.position = position;
         }
+
+        public void setId(int id) {
+            this.id = id;
+        }
     }
 
 
diff --git a/src/com/android/launcher3/allapps/LetterListTextView.java b/src/com/android/launcher3/allapps/LetterListTextView.java
new file mode 100644
index 0000000..9326d79
--- /dev/null
+++ b/src/com/android/launcher3/allapps/LetterListTextView.java
@@ -0,0 +1,133 @@
+/*
+ * 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.allapps;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Themes;
+
+/**
+ * A TextView that is used to display the letter list in the fast scroller.
+ */
+public class LetterListTextView extends TextView {
+    private static final float ABSOLUTE_TRANSLATION_X = 30f;
+    private static final float ABSOLUTE_SCALE = 1.4f;
+    private final Drawable mLetterBackground;
+    private final int mLetterListTextWidthAndHeight;
+    private final int mTextColor;
+    private final int mBackgroundColor;
+    private final int mSelectedColor;
+
+    public LetterListTextView(Context context) {
+        this(context, null, 0);
+    }
+
+    public LetterListTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public LetterListTextView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mLetterBackground = context.getDrawable(R.drawable.bg_letter_list_text);
+        mLetterListTextWidthAndHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.fastscroll_list_letter_size);
+        mTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurface);
+        mBackgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceContainer);
+        mSelectedColor = Themes.getAttrColor(context, R.attr.materialColorOnSecondary);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+        setBackground(mLetterBackground);
+        setTextColor(mTextColor);
+        setClickable(false);
+        setWidth(mLetterListTextWidthAndHeight);
+        setTextSize(mLetterListTextWidthAndHeight);
+        setVisibility(VISIBLE);
+    }
+
+    /**
+     * Animates the letter list text view based on the current finger position.
+     *
+     * @param currentFingerY The Y position of where the finger is placed on the fastScroller in
+     *                       pixels.
+     */
+    public void animateBasedOnYPosition(int currentFingerY) {
+        if (getBackground() == null) {
+            return;
+        }
+        float cutOffMin = currentFingerY - (getHeight() * 2);
+        float cutOffMax = currentFingerY + (getHeight() * 2);
+        float cutOffDistance = cutOffMax - cutOffMin;
+        // Update the background blend color
+        boolean isWithinAnimationBounds = getY() < cutOffMax && getY() > cutOffMin;
+        if (isWithinAnimationBounds) {
+            getBackground().setColorFilter(new PorterDuffColorFilter(
+                    getBlendColorBasedOnYPosition(currentFingerY, cutOffDistance),
+                    PorterDuff.Mode.MULTIPLY));
+        } else {
+            getBackground().setColorFilter(new PorterDuffColorFilter(
+                    mBackgroundColor, PorterDuff.Mode.MULTIPLY));
+        }
+        translateBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds);
+        scaleBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds);
+    }
+
+    private int getBlendColorBasedOnYPosition(int y, float cutOffDistance) {
+        float raisedCosineBlend = (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI);
+        float blendRatio = Utilities.boundToRange(raisedCosineBlend, 0f, 1f);
+        return ColorUtils.blendARGB(mBackgroundColor, mSelectedColor, blendRatio);
+    }
+
+    private void scaleBasedOnYPosition(int y, float cutOffDistance,
+            boolean isWithinAnimationBounds) {
+        float raisedCosineScale = (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI)
+                * ABSOLUTE_SCALE;
+        if (isWithinAnimationBounds) {
+            raisedCosineScale = Utilities.boundToRange(raisedCosineScale, 1f, ABSOLUTE_SCALE);
+            setScaleX(raisedCosineScale);
+            setScaleY(raisedCosineScale);
+        } else {
+            setScaleX(1);
+            setScaleY(1);
+        }
+    }
+
+    private void translateBasedOnYPosition(int y, float cutOffDistance,
+            boolean isWithinAnimationBounds) {
+        float raisedCosineTranslation =
+                (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI)
+                        * ABSOLUTE_TRANSLATION_X;
+        if (isWithinAnimationBounds) {
+            raisedCosineTranslation = -1 * Utilities.boundToRange(raisedCosineTranslation,
+                    0, ABSOLUTE_TRANSLATION_X);
+            setTranslationX(raisedCosineTranslation);
+        } else {
+            setTranslationX(0);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
index 650df5a..e218b4d 100644
--- a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
+++ b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
@@ -17,6 +17,7 @@
 package com.android.launcher3.debug
 
 import android.content.Context
+import android.util.Log
 import com.android.launcher3.util.MainThreadInitializedObject
 import com.android.launcher3.util.SafeCloseable
 
@@ -50,5 +51,7 @@
 
     override fun close() {}
 
-    override fun sendEvent(event: TestEvent) {}
+    override fun sendEvent(event: TestEvent) {
+        Log.d("TestEventsEmitterProduction", "Event sent ${event.event}")
+    }
 }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 3edf1f2..7bec768 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -773,6 +773,7 @@
         addAnimationStartListeners(anim);
         // Because t=0 has the folder match the folder icon, we can skip the
         // first frame and have the same movement one frame earlier.
+        Log.d("b/311077782", "Folder.animateOpen");
         anim.setCurrentPlayTime(Math.min(getSingleFrameMs(getContext()), anim.getTotalDuration()));
         anim.start();
 
diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
index 406f697..de2269c 100644
--- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
+++ b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
@@ -18,10 +18,12 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.LauncherActivityInfo;
+import android.os.Build;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BaseIconFactory.IconOptions;
@@ -64,9 +66,16 @@
     @Override
     public BitmapInfo loadIcon(@NonNull Context context, @NonNull LauncherActivityInfo object) {
         try (LauncherIcons li = LauncherIcons.obtain(context)) {
-            return li.createBadgedIconBitmap(LauncherAppState.getInstance(context)
-                            .getIconProvider().getIcon(object, li.mFillResIconDpi),
-                    new IconOptions().setUser(object.getUser()));
+            IconOptions iconOptions = new IconOptions().setUser(object.getUser());
+            iconOptions.mIsArchived = Flags.useNewIconForArchivedApps()
+                && Build.VERSION.SDK_INT >= 35
+                && object.getActivityInfo().isArchived;
+            return li.createBadgedIconBitmap(
+                    LauncherAppState.getInstance(context)
+                        .getIconProvider()
+                        .getIcon(object, li.mFillResIconDpi),
+                    iconOptions
+            );
         }
     }
 }
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index 551c2d8..59d1d00 100644
--- a/src/com/android/launcher3/model/ItemInstallQueue.java
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -192,22 +192,18 @@
     }
 
     private void queuePendingShortcutInfo(PendingInstallShortcutInfo info) {
-        final Exception stackTrace = new Exception();
 
         // Queue the item up for adding if launcher has not loaded properly yet
         MODEL_EXECUTOR.post(() -> {
             Pair<ItemInfo, Object> itemInfo = info.getItemInfo(mContext);
             if (itemInfo == null) {
                 FileLog.d(LOG,
-                        "Adding PendingInstallShortcutInfo with no attached info to queue.",
-                        stackTrace);
+                        "Adding PendingInstallShortcutInfo with no attached info to queue.");
             } else {
                 FileLog.d(LOG,
-                        "Adding PendingInstallShortcutInfo to queue. Attached info: "
-                                + itemInfo.first,
-                        stackTrace);
+                        "Adding PendingInstallShortcutInfo to queue."
+                                + " Attached info: " + itemInfo.first);
             }
-
             addToQueue(info);
         });
         flushInstallQueue();
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 269cb9f..605accf 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -209,7 +209,10 @@
                 mApp.getContext().getContentResolver(),
                 "launcher_broadcast_installed_apps",
                 /* def= */ 0);
-        if (launcherBroadcastInstalledApps == 1 && mIsRestoreFromBackup) {
+        boolean shouldAttachArchivingExtras = mIsRestoreFromBackup
+                && (launcherBroadcastInstalledApps == 1
+                        || Flags.enableFirstScreenBroadcastArchivingExtras());
+        if (shouldAttachArchivingExtras) {
             List<FirstScreenBroadcastModel> broadcastModels =
                     FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
                             mPmHelper,
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index c117be4..856c294 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -25,6 +25,7 @@
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.os.Build;
 import android.os.UserHandle;
+import android.util.Log;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -32,6 +33,7 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.Flags;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.lang.ref.WeakReference;
@@ -41,6 +43,8 @@
 @WorkerThread
 public class InstallSessionTracker extends PackageInstaller.SessionCallback {
 
+    public static final String TAG = "InstallSessionTracker";
+
     // Lazily initialized
     private SparseArray<PackageUserKey> mActiveSessions = null;
 
@@ -75,6 +79,11 @@
         }
         SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback);
         if (sessionInfo != null) {
+            FileLog.d(TAG, "onCreated: Install session created for"
+                    + " appPackageName=" + sessionInfo.getAppPackageName()
+                    + ", sessionId=" + sessionInfo.getSessionId()
+                    + ", appIcon=" + sessionInfo.getAppIcon()
+                    + ", appLabel=" + sessionInfo.getAppLabel());
             callback.onInstallSessionCreated(PackageInstallInfo.fromInstallingState(sessionInfo));
         }
 
@@ -102,6 +111,10 @@
         activeSessions.remove(sessionId);
 
         if (key != null && key.mPackageName != null) {
+            FileLog.d(TAG, "onFinished: active install session finished for"
+                    + " appPackageName=" + key.mPackageName
+                    + ", sessionId=" + sessionId
+                    + ", success=" + success);
             String packageName = key.mPackageName;
             PackageInstallInfo info = PackageInstallInfo.fromState(
                     success ? STATUS_INSTALLED : STATUS_FAILED,
@@ -141,6 +154,11 @@
         }
         SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback);
         if (sessionInfo != null) {
+            Log.d(TAG, "onBadgingChanged: badging info changed for"
+                    + " appPackageName=" + sessionInfo.getAppPackageName()
+                    + ", sessionId=" + sessionInfo.getSessionId()
+                    + ", appIcon=" + sessionInfo.getAppIcon()
+                    + ", appLabel=" + sessionInfo.getAppLabel());
             helper.tryQueuePromiseAppIcon(sessionInfo);
         }
     }
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 1e577be..37482ac 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -175,8 +175,9 @@
                 mLauncher.getDeviceProfile(), taskViewDrawAlpha);
 
         if (mFadeOutView != null) {
-            // The alpha goes from 1 to 0 when progress is 0 and 0.33 respectively.
-            mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.33f, 0, 1, LINEAR)));
+            // The alpha goes from 1 to 0 when progress is 0 and 0.15 respectively.
+            // This value minimizes view display time while still allowing the view to fade out.
+            mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.15f, 0, 1, LINEAR)));
         }
     }
 
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index fa17b7b..63648dd 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -20,6 +20,9 @@
 
 import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
 
+import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.ALL_APPS_SCROLLER;
+import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER;
+
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
@@ -40,11 +43,15 @@
 import android.view.WindowInsets;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.FastScrollRecyclerView;
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.LetterListTextView;
 import com.android.launcher3.graphics.FastScrollThumbDrawable;
 import com.android.launcher3.util.Themes;
 
@@ -55,6 +62,19 @@
  * The track and scrollbar that shows when you scroll the list.
  */
 public class RecyclerViewFastScroller extends View {
+
+    /** FastScrollerLocation describes what RecyclerView the fast scroller is dedicated to. */
+    public enum FastScrollerLocation {
+        UNKNOWN_SCROLLER(0),
+        ALL_APPS_SCROLLER(1),
+        WIDGET_SCROLLER(2);
+
+        public final int location;
+
+        FastScrollerLocation(int location) {
+            this.location = location;
+        }
+    }
     private static final String TAG = "RecyclerViewFastScroller";
     private static final boolean DEBUG = false;
     private static final int FASTSCROLL_THRESHOLD_MILLIS = 40;
@@ -106,6 +126,8 @@
     private final Point mThumbDrawOffset = new Point();
 
     private final Paint mTrackPaint;
+    private final int mThumbColor;
+    private final int mThumbLetterScrollerColor;
 
     private float mLastTouchY;
     private boolean mIsDragging;
@@ -139,6 +161,7 @@
     private int mDownX;
     private int mDownY;
     private int mLastY;
+    private FastScrollerLocation mFastScrollerLocation;
 
     public RecyclerViewFastScroller(Context context) {
         this(context, null);
@@ -151,13 +174,16 @@
     public RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
 
+        mFastScrollerLocation = FastScrollerLocation.UNKNOWN_SCROLLER;
         mTrackPaint = new Paint();
         mTrackPaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
 
+        mThumbColor = Themes.getColorAccent(context);
+        mThumbLetterScrollerColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright);
         mThumbPaint = new Paint();
         mThumbPaint.setAntiAlias(true);
-        mThumbPaint.setColor(Themes.getColorAccent(context));
+        mThumbPaint.setColor(mThumbColor);
         mThumbPaint.setStyle(Paint.Style.FILL);
 
         Resources res = getResources();
@@ -334,6 +360,18 @@
         animatePopupVisibility(!TextUtils.isEmpty(sectionName));
         mLastTouchY = boundedY;
         setThumbOffsetY((int) mLastTouchY);
+        updateFastScrollerLetterList(y);
+    }
+
+    private void updateFastScrollerLetterList(int y) {
+        if (!shouldUseLetterFastScroller()) {
+            return;
+        }
+        ConstraintLayout mLetterList = mRv.getLetterList();
+        for (int i = 0; i < mLetterList.getChildCount(); i++) {
+            LetterListTextView currentLetter = (LetterListTextView) mLetterList.getChildAt(i);
+            currentLetter.animateBasedOnYPosition(y + mTouchOffsetY);
+        }
     }
 
     /** End any active fast scrolling touch handling, if applicable. */
@@ -359,15 +397,35 @@
         mThumbDrawOffset.set(getWidth() / 2, mRv.getScrollBarTop());
         // Draw the track
         float halfW = mWidth / 2;
-        canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(),
-                mWidth, mWidth, mTrackPaint);
-
-        canvas.translate(0, mThumbOffsetY);
+        boolean useLetterFastScroller = shouldUseLetterFastScroller();
+        if (useLetterFastScroller) {
+            float translateX;
+            if (mIsDragging) {
+                // halfW * 3 is half circle.
+                translateX = halfW * 3;
+            } else {
+                translateX = halfW * 5;
+            }
+            canvas.translate(translateX, mThumbOffsetY);
+        } else {
+            canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(),
+                    mWidth, mWidth, mTrackPaint);
+            canvas.translate(0, mThumbOffsetY);
+        }
         mThumbDrawOffset.y += mThumbOffsetY;
+
+        /* Draw half circle */
         halfW += mThumbPadding;
         float r = getScrollThumbRadius();
-        mThumbBounds.set(-halfW, 0, halfW, mThumbHeight);
-        canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint);
+        if (useLetterFastScroller) {
+            mThumbPaint.setColor(mThumbLetterScrollerColor);
+            mThumbBounds.set(0, 0, 0, mThumbHeight);
+            canvas.drawCircle(-halfW, halfW, r * 2, mThumbPaint);
+        } else {
+            mThumbPaint.setColor(mThumbColor);
+            mThumbBounds.set(-halfW, 0, halfW, mThumbHeight);
+            canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint);
+        }
         mThumbBounds.roundOut(SYSTEM_GESTURE_EXCLUSION_RECT.get(0));
         // swiping very close to the thumb area (not just within it's bound)
         // will also prevent back gesture
@@ -380,6 +438,11 @@
         canvas.restoreToCount(saveCount);
     }
 
+    boolean shouldUseLetterFastScroller() {
+        return Flags.letterFastScroller()
+                && getScrollerLocation() == FastScrollerLocation.ALL_APPS_SCROLLER;
+    }
+
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mSystemGestureInsets = insets.getSystemGestureInsets();
@@ -421,19 +484,25 @@
         return isNearThumb(x, y);
     }
 
-    /**
-     * Returns whether the specified x position is near the scroll bar.
-     */
-    public boolean isNearScrollBar(int x) {
-        return x >= (getWidth() - mMaxWidth) / 2 - mScrollbarLeftOffsetTouchDelegate
-                && x <= (getWidth() + mMaxWidth) / 2;
+    public FastScrollerLocation getScrollerLocation() {
+        return mFastScrollerLocation;
+    }
+
+    public void setFastScrollerLocation(@NonNull FastScrollerLocation location) {
+        mFastScrollerLocation = location;
     }
 
     private void animatePopupVisibility(boolean visible) {
         if (mPopupVisible != visible) {
             mPopupVisible = visible;
-            mPopupView.animate().cancel();
-            mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
+            if (shouldUseLetterFastScroller()) {
+                mRv.getLetterList().animate().alpha(visible ? 1f : 0f)
+                        .setDuration(visible ? 200 : 150).start();
+            } else {
+                mPopupView.animate().cancel();
+                mPopupView.animate().alpha(visible ? 1f : 0f)
+                        .setDuration(visible ? 200 : 150).start();
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 2af8e6f..c8ad564 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
+import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -119,7 +120,7 @@
                     WidgetsRecyclerView searchRecyclerView =
                             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
                     if (mIsInSearchMode && searchRecyclerView != null) {
-                        searchRecyclerView.bindFastScrollbar(mFastScroller);
+                        searchRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
                     }
                 }
 
@@ -276,7 +277,7 @@
     }
 
     private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
-        recyclerView.bindFastScrollbar(mFastScroller);
+        recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
         if (mCurrentWidgetsRecyclerView != recyclerView) {
             // Only reset the scroll position & expanded apps if the currently shown recycler view
             // has been updated.
@@ -290,10 +291,10 @@
     protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
         // The first item is always an empty space entry. Look for any more items.
         boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
-        adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
 
         if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) {
             mNoWidgetsView.setText(R.string.no_search_results);
+            adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
         } else if (adapterHolder.mAdapterType == AdapterHolder.WORK
                 && mUserCache.getUserProfiles().stream()
                 .filter(userHandle -> mUserCache.getUserInfo(userHandle).isWork())
@@ -556,6 +557,8 @@
             mNoWidgetsView.setVisibility(GONE);
         } else {
             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
+            mAdapters.get(getCurrentAdapterHolderType()).mWidgetsRecyclerView.setVisibility(
+                    VISIBLE);
             // Visibility of recommended widgets, recycler views and headers are handled in methods
             // below.
             post(this::onRecommendedWidgetsBound);
@@ -1057,7 +1060,7 @@
             mWidgetsRecyclerView.setClipToOutline(true);
             mWidgetsRecyclerView.setClipChildren(false);
             mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
-            mWidgetsRecyclerView.bindFastScrollbar(mFastScroller);
+            mWidgetsRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
             mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator);
             mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
             if (!isTwoPane()) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 2653514..d329674 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -461,6 +461,13 @@
         if (!isWidgetAvailable) {
             mRightPane.removeAllViews();
             mRightPane.addView(mNoWidgetsView);
+            // with no widgets message, no header is selected on left
+            if (mSuggestedWidgetsPackageUserKey != null
+                    && mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)
+                    && mSuggestedWidgetsHeader != null) {
+                mSuggestedWidgetsHeader.setExpanded(false);
+            }
+            mSelectedHeader = null;
         }
         super.updateRecyclerViewVisibility(adapterHolder);
     }
diff --git a/tests/Android.bp b/tests/Android.bp
index 9945570..1fa6e05 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -160,7 +160,7 @@
 }
 
 filegroup {
-    name: "launcher-testing-helpers-multivalent",
+    name: "launcher-testing-helpers-robo",
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
@@ -183,7 +183,7 @@
 filegroup {
     name: "launcher-testing-helpers",
     srcs: [
-        ":launcher-testing-helpers-multivalent",
+        ":launcher-testing-helpers-robo",
         "src/**/RoboApiWrapper.kt",
     ],
 }
@@ -195,7 +195,7 @@
         ":launcher3-robo-src",
 
         // Test util classes
-        ":launcher-testing-helpers-multivalent",
+        ":launcher-testing-helpers-robo",
         ":launcher-testing-shared",
     ],
     exclude_srcs: [
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
index 30953cc..a62258c 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -17,6 +17,9 @@
 
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -37,6 +40,8 @@
 import com.android.launcher3.celllayout.testgenerator.RandomBoardGenerator;
 import com.android.launcher3.celllayout.testgenerator.RandomMultiBoardGenerator;
 import com.android.launcher3.util.ActivityContextWrapper;
+import com.android.launcher3.util.rule.TestStabilityRule;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.views.DoubleShadowBubbleTextView;
 
 import org.junit.Rule;
@@ -68,12 +73,16 @@
     private Context mApplicationContext;
 
     @Rule
+    public TestStabilityRule mTestStabilityRule = new TestStabilityRule();
+
+    @Rule
     public UnitTestCellLayoutBuilderRule mCellLayoutBuilder = new UnitTestCellLayoutBuilderRule();
 
     /**
      * This test reads existing test cases and makes sure the CellLayout produces the same
      * output for each of them for a given input.
      */
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     public void testAllCases() throws IOException {
         List<ReorderAlgorithmUnitTestCase> testCases = getTestCases(
@@ -116,6 +125,7 @@
     /**
      * Same as above but testing the Multipage CellLayout.
      */
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     public void generateValidTests_Multi() {
         Random generator = new Random(SEED);
diff --git a/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java b/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java
deleted file mode 100644
index 10e0be8..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2021 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.settings;
-
-import static androidx.preference.PreferenceFragmentCompat.ARG_PREFERENCE_ROOT;
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem;
-import static androidx.test.espresso.intent.Intents.intended;
-import static androidx.test.espresso.intent.matcher.BundleMatchers.hasEntry;
-import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent;
-import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
-import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-
-import static com.android.launcher3.settings.SettingsActivity.DEVELOPER_OPTIONS_KEY;
-import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARGS;
-import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ROOT_KEY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.equalTo;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-
-import androidx.test.core.app.ActivityScenario;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.intent.Intents;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.launcher3.R;
-import com.android.systemui.shared.plugins.PluginPrefs;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class SettingsActivityTest {
-
-    private Context mApplicationContext;
-
-    @Before
-    public void setUp() {
-        mApplicationContext = ApplicationProvider.getApplicationContext();
-        Intents.init();
-    }
-
-    @After
-    public void tearDown() {
-        Intents.release();
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_aboutTap_launchesActivity() {
-        ActivityScenario.launch(SettingsActivity.class);
-        onView(withId(R.id.recycler_view)).perform(
-                actionOnItem(hasDescendant(withText("About")), click()));
-
-        intended(allOf(
-                hasComponent(SettingsActivity.class.getName()),
-                hasExtra(
-                        equalTo(EXTRA_FRAGMENT_ARGS),
-                        hasEntry(ARG_PREFERENCE_ROOT, "about_screen"))));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_developerOptionsTap_launchesActivityWithFragment() {
-        PluginPrefs.setHasPlugins(mApplicationContext);
-        ActivityScenario.launch(SettingsActivity.class);
-        onView(withId(R.id.recycler_view)).perform(
-                actionOnItem(hasDescendant(withText("Developer Options")), click()));
-
-        intended(allOf(
-                hasComponent(SettingsActivity.class.getName()),
-                hasExtra(EXTRA_FRAGMENT_ROOT_KEY, DEVELOPER_OPTIONS_KEY)));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_aboutScreenIntent() {
-        Bundle fragmentArgs = new Bundle();
-        fragmentArgs.putString(ARG_PREFERENCE_ROOT, "about_screen");
-
-        Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
-                .putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs);
-        ActivityScenario.launch(intent);
-
-        onView(withText("About")).check(matches(isDisplayed()));
-        onView(withText("Version")).check(matches(isDisplayed()));
-        onView(withContentDescription("Navigate up")).check(matches(isDisplayed()));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_developerOptionsFragmentIntent() {
-        Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
-                .putExtra(EXTRA_FRAGMENT_ROOT_KEY, DEVELOPER_OPTIONS_KEY);
-        ActivityScenario.launch(intent);
-
-        onView(withText("Developer Options")).check(matches(isDisplayed()));
-        onView(withId(R.id.filter_box)).check(matches(isDisplayed()));
-        onView(withContentDescription("Navigate up")).check(matches(isDisplayed()));
-    }
-
-    @Test
-    @Ignore  // b/199309785
-    public void testSettings_backButtonFinishesActivity() {
-        Bundle fragmentArgs = new Bundle();
-        fragmentArgs.putString(ARG_PREFERENCE_ROOT, "about_screen");
-        Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
-                .putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs);
-        ActivityScenario<SettingsActivity> scenario = ActivityScenario.launch(intent);
-
-        onView(withContentDescription("Navigate up")).perform(click());
-        scenario.onActivity(activity -> assertThat(activity.isFinishing()).isTrue());
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index 909aabd..ad2d8c2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -109,6 +109,9 @@
                     getPackageManager().
                     getPackageInfo(launcherPackageName, 0)
                     .versionName;
+            if (launcherVersion == null) {
+                return LOCAL;
+            }
         } catch (PackageManager.NameNotFoundException e) {
             throw new RuntimeException(e);
         }
diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
index 05a1224..c7c9dbb 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
@@ -25,7 +25,6 @@
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.launcher3.Launcher;
@@ -33,7 +32,6 @@
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
-import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.Test;
 
@@ -191,7 +189,6 @@
     /**
      * Makes sure that when pressing back when AllApps is open we go back to the Home screen.
      */
-    @FlakyTest(bugId = 256615483)
     @Test
     @PortraitLandscape
     public void testPressBackFromAllAppsToHome() {
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
index 4cecb5a..bcb9191 100644
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
@@ -21,6 +21,7 @@
 import android.view.View
 import android.view.ViewGroup
 import com.android.launcher3.CellLayout
+import com.android.launcher3.Utilities
 import com.android.launcher3.Workspace
 import com.android.launcher3.util.CellAndSpan
 import com.android.launcher3.widget.LauncherAppWidgetHostView
@@ -54,7 +55,7 @@
         return view as LauncherAppWidgetHostView
     }
 
-    fun getCellTopLeftRelativeToCellLayout(
+    fun getCellTopLeftRelativeToWorkspace(
         workspace: Workspace<*>,
         cellAndSpan: CellAndSpan
     ): Point {
@@ -67,6 +68,8 @@
             cellAndSpan.spanY,
             target
         )
-        return Point(target.left, target.top)
+        val point = floatArrayOf(target.left.toFloat(), target.top.toFloat())
+        Utilities.getDescendantCoordRelativeToAncestor(cellLayout, workspace, point, false)
+        return Point(point[0].toInt(), point[1].toInt())
     }
 }