Merge "Honors three_button_corner_swipe flag." into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 5b00b5d..40c3797 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -333,3 +333,30 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "letter_fast_scroller"
+    namespace: "launcher"
+    description: "Change fast scroller to a lettered list"
+    bug: "358673724"
+}
+
+flag {
+    name: "enable_desktop_task_alpha_animation"
+    namespace: "launcher"
+    description: "Enables the animation of the desktop task's background view"
+    bug: "320307666"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "ignore_three_finger_trackpad_for_nav_handle_long_press"
+    namespace: "launcher"
+    description: "Ignore three finger trackpad event for nav handle long press"
+    bug: "342143522"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml
index 5fec1e3..1e5df41 100644
--- a/quickstep/res/values-cs/strings.xml
+++ b/quickstep/res/values-cs/strings.xml
@@ -92,7 +92,7 @@
     <string name="allset_hint" msgid="459504134589971527">"Přejetím nahoru se vrátíte na plochu"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Klepnutím na tlačítko plochy se vrátíte na plochu"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> můžete začít používat"</string>
-    <string name="default_device_name" msgid="6660656727127422487">"zařízení"</string>
+    <string name="default_device_name" msgid="6660656727127422487">"Zařízení"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Nastavení navigace v systému"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Sdílet"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Snímek obrazovky"</string>
diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml
index 1a84776..b68378b 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -97,7 +97,7 @@
     <string name="action_share" msgid="2648470652637092375">"Partager"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Capture d\'écran"</string>
     <string name="action_split" msgid="2098009717623550676">"Partager"</string>
-    <string name="action_save_app_pair" msgid="5974823919237645229">"Enregistrer la paire d\'applis"</string>
+    <string name="action_save_app_pair" msgid="5974823919237645229">"Enregistrer une paire d\'applis"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Appuyez sur autre appli pour l\'écran partagé"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Sélectionnez une autre appli pour utiliser l\'écran partagé."</string>
     <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Annuler"</string>
diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml
index 6fa5cc6..6178570 100644
--- a/quickstep/res/values-hr/strings.xml
+++ b/quickstep/res/values-hr/strings.xml
@@ -92,7 +92,7 @@
     <string name="allset_hint" msgid="459504134589971527">"Prijeđite prstom prema gore da biste otvorili početni zaslon"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Dodirnite gumb početnog zaslona da biste prešli na početni zaslon"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> je spreman za početak upotrebe"</string>
-    <string name="default_device_name" msgid="6660656727127422487">"uređaj"</string>
+    <string name="default_device_name" msgid="6660656727127422487">"Uređaj"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Postavke navigacije sustavom"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Podijeli"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Snimka zaslona"</string>
diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml
index 4b5f71f..2be5e2b 100644
--- a/quickstep/res/values-sq/strings.xml
+++ b/quickstep/res/values-sq/strings.xml
@@ -22,8 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Gozhdo"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Formë e lirë"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktopi"</string>
-    <!-- no translation found for recent_task_desktop (8081113562549637334) -->
-    <skip />
+    <string name="recent_task_desktop" msgid="8081113562549637334">"Desktopi"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nuk ka asnjë artikull të fundit"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Cilësimet e përdorimit të aplikacionit"</string>
     <string name="recents_clear_all" msgid="5328176793634888831">"Pastroji të gjitha"</string>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index e691134..d981882 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -376,6 +376,7 @@
     <dimen name="transient_taskbar_stash_spring_velocity_dp_per_s">400dp</dimen>
     <dimen name="taskbar_tooltip_vertical_padding">8dp</dimen>
     <dimen name="taskbar_tooltip_horizontal_padding">16dp</dimen>
+    <dimen name="taskbar_tooltip_y_offset">4dp</dimen>
 
     <!-- An additional touch slop to prevent x-axis movement during the swipe up to show taskbar -->
     <dimen name="transient_taskbar_clamped_offset_bound">16dp</dimen>
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/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index b63b9dd..96a6d28 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -248,7 +248,7 @@
         }
 
         mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, isVisible);
-        if (fromInit) {
+        if (fromInit || mControllers == null) {
             duration = 0;
         }
         return mTaskbarLauncherStateController.applyState(duration, startAnimation);
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index f61840a..a979d58 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -63,7 +63,6 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.Region.Op;
-import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PaintDrawable;
 import android.graphics.drawable.RotateDrawable;
@@ -74,8 +73,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
-import android.view.View.OnClickListener;
-import android.view.View.OnHoverListener;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
@@ -106,7 +103,6 @@
 import com.android.systemui.shared.navigationbar.KeyButtonRipple;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
 import com.android.systemui.shared.rotation.RotationButton;
-import com.android.systemui.shared.rotation.RotationButtonController;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -304,8 +300,13 @@
                         .get(ALPHA_INDEX_SMALL_SCREEN),
                 flags -> (flags & FLAG_SMALL_SCREEN) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
-                .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        if (!mContext.isPhoneMode()) {
+            mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
+                    .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        }
+
+        // Start at 1 because relevant flags are unset at init.
+        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
 
         // Force nav buttons (specifically back button) to be visible during setup wizard.
         boolean isInSetup = !mContext.isUserSetupComplete();
@@ -317,39 +318,41 @@
         // - IME is showing (add separate translation for IME)
         // - VoiceInteractionWindow (assistant) is showing
         // - Keyboard shortcuts helper is showing
-        int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
-                | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
-        mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
-                flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
-                1, 0));
-        // Center nav buttons in new height for IME.
-        float transForIme = (mContext.getDeviceProfile().taskbarHeight
-                - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
-        // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
-        float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
-        mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
-                flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
-                transForIme, defaultButtonTransY));
+        if (!mContext.isPhoneMode()) {
+            int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
+                    | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
+            mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
+                    flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
+                    1, 0));
+            // Center nav buttons in new height for IME.
+            float transForIme = (mContext.getDeviceProfile().taskbarHeight
+                    - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
+            // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
+            float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
+            mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
+                    flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
+                    transForIme, defaultButtonTransY));
 
-        // Start at 1 because relevant flags are unset at init.
-        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
-        mPropertyHolders.add(new StatePropertyHolder(
-                mOnBackgroundNavButtonColorOverrideMultiplier,
-                flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mOnBackgroundNavButtonColorOverrideMultiplier,
+                    flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(
-                mSlideInViewVisibleNavButtonColorOverride,
-                flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mSlideInViewVisibleNavButtonColorOverride,
+                    flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+        }
 
         if (alwaysShowButtons) {
             initButtons(mNavButtonContainer, mEndContextualContainer,
                     mControllers.navButtonController);
             updateButtonLayoutSpacing();
-            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneButtonNavMode());
+            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneMode());
 
-            mPropertyHolders.add(new StatePropertyHolder(
-                    mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
-                    flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            if (!mContext.isPhoneMode()) {
+                mPropertyHolders.add(new StatePropertyHolder(
+                        mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
+                        flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            }
         } else if (!mIsImeRenderingNavButtons) {
             View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
                     mStartContextualContainer, mControllers.navButtonController, R.id.back);
@@ -711,7 +714,7 @@
     private void applyState() {
         int count = mPropertyHolders.size();
         for (int i = 0; i < count; i++) {
-            mPropertyHolders.get(i).setState(mState);
+            mPropertyHolders.get(i).setState(mState, mContext.isGestureNav());
         }
     }
 
@@ -1177,83 +1180,6 @@
         }
     }
 
-    private class RotationButtonImpl implements RotationButton {
-
-        private final ImageView mButton;
-        private AnimatedVectorDrawable mImageDrawable;
-
-        RotationButtonImpl(ImageView button) {
-            mButton = button;
-        }
-
-        @Override
-        public void setRotationButtonController(RotationButtonController rotationButtonController) {
-            // TODO(b/187754252) UI polish, different icons based on light/dark context, etc
-            mImageDrawable = (AnimatedVectorDrawable) mButton.getContext()
-                    .getDrawable(rotationButtonController.getIconResId());
-            mButton.setImageDrawable(mImageDrawable);
-            mButton.setContentDescription(mButton.getResources()
-                    .getString(R.string.accessibility_rotate_button));
-            mImageDrawable.setCallback(mButton);
-        }
-
-        @Override
-        public View getCurrentView() {
-            return mButton;
-        }
-
-        @Override
-        public boolean show() {
-            mButton.setVisibility(View.VISIBLE);
-            mState |= FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean hide() {
-            mButton.setVisibility(View.GONE);
-            mState &= ~FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean isVisible() {
-            return mButton.getVisibility() == View.VISIBLE;
-        }
-
-        @Override
-        public void updateIcon(int lightIconColor, int darkIconColor) {
-            // TODO(b/187754252): UI Polish
-        }
-
-        @Override
-        public void setOnClickListener(OnClickListener onClickListener) {
-            mButton.setOnClickListener(onClickListener);
-        }
-
-        @Override
-        public void setOnHoverListener(OnHoverListener onHoverListener) {
-            mButton.setOnHoverListener(onHoverListener);
-        }
-
-        @Override
-        public AnimatedVectorDrawable getImageDrawable() {
-            return mImageDrawable;
-        }
-
-        @Override
-        public void setDarkIntensity(float darkIntensity) {
-            // TODO(b/187754252) UI polish
-        }
-
-        @Override
-        public boolean acceptRotationProposal() {
-            return mButton.isAttachedToWindow();
-        }
-    }
-
     private static class StatePropertyHolder {
 
         private final float mEnabledValue, mDisabledValue;
@@ -1284,13 +1210,16 @@
             mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue);
         }
 
-        public void setState(int flags) {
+        public void setState(int flags, boolean skipAnimation) {
             boolean isEnabled = mEnableCondition.test(flags);
             if (mIsEnabled != isEnabled) {
                 mIsEnabled = isEnabled;
                 mAnimator.cancel();
                 mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue);
                 mAnimator.start();
+                if (skipAnimation) {
+                    mAnimator.end();
+                }
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
index 94e2244..caf3320 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
@@ -47,6 +47,7 @@
     private final int[] mTmpArr = new int[2];
 
     private @Nullable ObjectAnimator mColorChangeAnim;
+    private Boolean mIsRegionDark;
 
     public StashedHandleView(Context context) {
         this(context, null);
@@ -95,7 +96,11 @@
      * @param animate Whether to animate the change, or apply it immediately.
      */
     public void updateHandleColor(boolean isRegionDark, boolean animate) {
+        if (mIsRegionDark != null && mIsRegionDark == isRegionDark) {
+            return;
+        }
         int newColor = isRegionDark ? mStashedHandleLightColor : mStashedHandleDarkColor;
+        mIsRegionDark = isRegionDark;
         if (mColorChangeAnim != null) {
             mColorChangeAnim.cancel();
         }
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/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index 8c7879d..3bff31f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -50,6 +50,7 @@
     private final View mHoverView;
     private final ArrowTipView mHoverToolTipView;
     private final String mToolTipText;
+    private final int mYOffset;
 
     public TaskbarHoverToolTipController(TaskbarActivityContext activity, TaskbarView taskbarView,
             View hoverView) {
@@ -79,6 +80,8 @@
         mHoverToolTipView.findViewById(R.id.text).setPadding(horizontalPadding, verticalPadding,
                 horizontalPadding, verticalPadding);
         mHoverToolTipView.setAlpha(0);
+        mYOffset = arrowContextWrapper.getResources().getDimensionPixelSize(
+                R.dimen.taskbar_tooltip_y_offset);
 
         AnimatorSet hoverOpenAnimator = new AnimatorSet();
         ObjectAnimator alphaOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0f, 1f);
@@ -89,7 +92,7 @@
         mHoverToolTipView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     mHoverToolTipView.setPivotY(bottom);
-                    mHoverToolTipView.setY(mTaskbarView.getTop() - (bottom - top));
+                    mHoverToolTipView.setY(mTaskbarView.getTop() - mYOffset - (bottom - top));
                 });
     }
 
@@ -121,6 +124,6 @@
         }
         Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
         mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
-                mTaskbarView.getTop(), /* shouldAutoClose= */ false);
+                mTaskbarView.getTop() - mYOffset, /* shouldAutoClose= */ false);
     }
 }
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 a5395d9..0eb8890 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;
@@ -245,7 +246,9 @@
 
         resetIconAlignment();
 
-        mLauncher.getStateManager().addStateListener(mStateListener);
+        if (!mControllers.taskbarActivityContext.isPhoneMode()) {
+            mLauncher.getStateManager().addStateListener(mStateListener);
+        }
         mLauncherState = launcher.getStateManager().getState();
         updateStateForSysuiFlags(sysuiStateFlags, /*applyState*/ false);
 
@@ -351,8 +354,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);
 
@@ -409,7 +414,7 @@
     }
 
     public Animator applyState(long duration, boolean start) {
-        if (mIsDestroyed) {
+        if (mIsDestroyed || mControllers.taskbarActivityContext.isPhoneMode()) {
             return null;
         }
         Animator animator = null;
@@ -858,7 +863,8 @@
                 "%s\tmTaskbarBackgroundAlpha=%.2f", prefix, mTaskbarBackgroundAlpha.value));
         pw.println(String.format(
                 "%s\tmIconAlphaForHome=%.2f", prefix, mIconAlphaForHome.getValue()));
-        pw.println(String.format("%s\tmPrevState=%s", prefix, getStateString(mPrevState)));
+        pw.println(String.format("%s\tmPrevState=%s", prefix,
+                mPrevState == null ? null : getStateString(mPrevState)));
         pw.println(String.format("%s\tmState=%s", prefix, getStateString(mState)));
         pw.println(String.format("%s\tmLauncherState=%s", prefix, mLauncherState));
         pw.println(String.format(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index b48ed60..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.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index d3b918e..27c1d9c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -150,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;
 
@@ -218,8 +219,8 @@
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
             // This gets modified in NavbarButtonsViewController, but the initial value it reads
             // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here
-            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN)
-                    .animateToValue(mActivity.isPhoneButtonNavMode() ? 0 : 1).start();
+            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN).setValue(
+                    mActivity.isPhoneMode() ? 0 : 1);
         }
         if (enableTaskbarPinning()) {
             mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener);
@@ -687,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);
         }
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/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 5392b70..3d94442 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -734,11 +734,18 @@
     }
 
     private void maybeUpdateRecentsAttachedState() {
-        maybeUpdateRecentsAttachedState(true /* animate */);
+        maybeUpdateRecentsAttachedState(/* animate= */ true);
     }
 
     protected void maybeUpdateRecentsAttachedState(boolean animate) {
-        maybeUpdateRecentsAttachedState(animate, false /* moveRunningTask */);
+        maybeUpdateRecentsAttachedState(animate, /* moveRunningTask= */ false);
+    }
+
+    protected void maybeUpdateRecentsAttachedState(boolean animate, boolean moveRunningTask) {
+        maybeUpdateRecentsAttachedState(
+                animate,
+                moveRunningTask,
+                mRecentsView != null && mRecentsView.shouldUpdateRunningTaskAlpha());
     }
 
     /**
@@ -749,8 +756,10 @@
      * Note this method has no effect unless the navigation mode is NO_BUTTON.
      * @param animate whether to animate when attaching RecentsView
      * @param moveRunningTask whether to move running task to front when attaching
+     * @param updateRunningTaskAlpha Whether to update the running task's attached alpha
      */
-    private void maybeUpdateRecentsAttachedState(boolean animate, boolean moveRunningTask) {
+    private void maybeUpdateRecentsAttachedState(
+            boolean animate, boolean moveRunningTask, boolean updateRunningTaskAlpha) {
         if ((!mDeviceState.isFullyGesturalNavMode() && !mGestureState.isTrackpadGesture())
                 || mRecentsView == null) {
             return;
@@ -781,7 +790,8 @@
             // TaskView jumping to new position as we move the tasks.
             mRecentsView.moveRunningTaskToFront();
         }
-        mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+        mAnimationFactory.setRecentsAttachedToAppWindow(
+                recentsAttachedToAppWindow, animate, updateRunningTaskAlpha);
 
         // Reapply window transform throughout the attach animation, as the animation affects how
         // much the window is bound by overscroll (vs moving freely).
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 293944d..8703843 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -21,11 +21,13 @@
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_ATTACHED_ALPHA_ANIM;
 import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
 import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import android.animation.Animator;
@@ -187,8 +189,10 @@
          * @param attached Whether to show RecentsView alongside the app window. If false, recents
          *                 will be hidden by some property we can animate, e.g. alpha.
          * @param animate Whether to animate recents to/from its new attached state.
+         * @param updateRunningTaskAlpha Whether to update the running task's attached alpha
          */
-        default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
+        default void setRecentsAttachedToAppWindow(
+                boolean attached, boolean animate, boolean updateRunningTaskAlpha) { }
 
         default boolean isRecentsAttachedToAppWindow() {
             return false;
@@ -253,12 +257,14 @@
             // (because we set the animation as the current state animation), so we reapply the
             // attached state here as well to ensure recents is shown/hidden appropriately.
             if (DisplayController.getNavigationMode(mActivity) == NavigationMode.NO_BUTTON) {
-                setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
+                setRecentsAttachedToAppWindow(
+                        mIsAttachedToWindow, false, recentsView.shouldUpdateRunningTaskAlpha());
             }
         }
 
         @Override
-        public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
+        public void setRecentsAttachedToAppWindow(
+                boolean attached, boolean animate, boolean updateRunningTaskAlpha) {
             if (mIsAttachedToWindow == attached && animate) {
                 return;
             }
@@ -266,6 +272,10 @@
                     .cancelStateElementAnimation(INDEX_RECENTS_FADE_ANIM);
             mActivity.getStateManager()
                     .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+            if (updateRunningTaskAlpha) {
+                mActivity.getStateManager()
+                        .cancelStateElementAnimation(INDEX_RECENTS_ATTACHED_ALPHA_ANIM);
+            }
 
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.addListener(new AnimatorListenerAdapter() {
@@ -280,19 +290,28 @@
 
             long animationDuration = animate ? RECENTS_ATTACH_DURATION : 0;
             Animator fadeAnim = mActivity.getStateManager()
-                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1f : 0f);
             fadeAnim.setInterpolator(attached ? INSTANT : ACCELERATE_2);
             fadeAnim.setDuration(animationDuration);
             animatorSet.play(fadeAnim);
 
             float fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(
                     mActivity.getOverviewPanel());
-            float toTranslation = attached ? 0 : 1;
-
+            float toTranslation = attached ? 0f : 1f;
             Animator translationAnimator = mActivity.getStateManager().createStateElementAnimation(
                     INDEX_RECENTS_TRANSLATE_X_ANIM, fromTranslation, toTranslation);
             translationAnimator.setDuration(animationDuration);
             animatorSet.play(translationAnimator);
+
+            if (updateRunningTaskAlpha) {
+                float fromAlpha = RUNNING_TASK_ATTACH_ALPHA.get(mActivity.getOverviewPanel());
+                float toAlpha = attached ? 1f : 0f;
+                Animator runningTaskAttachAlphaAnimator = mActivity.getStateManager()
+                        .createStateElementAnimation(
+                                INDEX_RECENTS_ATTACHED_ALPHA_ANIM, fromAlpha, toAlpha);
+                runningTaskAttachAlphaAnimator.setDuration(animationDuration);
+                animatorSet.play(runningTaskAttachAlphaAnimator);
+            }
             animatorSet.start();
         }
 
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/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 9510998..b321b3e 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -89,6 +89,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ConstantItem;
 import com.android.launcher3.EncryptionType;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.config.FeatureFlags;
@@ -1210,7 +1211,8 @@
             NavHandle navHandle = tac != null ? tac.getNavHandle()
                     : SystemUiProxy.INSTANCE.get(this);
             if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
-                    && navHandle.canNavHandleBeLongPressed()) {
+                    && navHandle.canNavHandleBeLongPressed()
+                    && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
                 reasonString.append(NEWLINE_PREFIX)
                         .append(reasonPrefix)
                         .append(SUBSTRING_PREFIX)
@@ -1306,6 +1308,11 @@
         return new CompoundString(NEWLINE_PREFIX).append(substring);
     }
 
+    private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
+        return Flags.ignoreThreeFingerTrackpadForNavHandleLongPress()
+                && gestureState.isThreeFingerTrackpadGesture();
+    }
+
     private void logInputConsumerSelectionReason(
             InputConsumer consumer, CompoundString reasonString) {
         ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ")
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/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/RecentsAtomicAnimationFactory.java b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
index 0b05c2e..63fe017 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.util;
 
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA;
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
@@ -33,8 +34,10 @@
 
     public static final int INDEX_RECENTS_FADE_ANIM = AtomicAnimationFactory.NEXT_INDEX + 0;
     public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = AtomicAnimationFactory.NEXT_INDEX + 1;
+    public static final int INDEX_RECENTS_ATTACHED_ALPHA_ANIM =
+            AtomicAnimationFactory.NEXT_INDEX + 2;
 
-    private static final int MY_ANIM_COUNT = 2;
+    private static final int MY_ANIM_COUNT = 3;
 
     protected final CONTAINER mContainer;
 
@@ -50,6 +53,7 @@
                 ObjectAnimator alpha = ObjectAnimator.ofFloat(mContainer.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
                 return alpha;
+            case INDEX_RECENTS_ATTACHED_ALPHA_ANIM:
             case INDEX_RECENTS_TRANSLATE_X_ANIM: {
                 RecentsView rv = mContainer.getOverviewPanel();
                 return new SpringAnimationBuilder(mContainer)
@@ -57,7 +61,8 @@
                         .setDampingRatio(0.8f)
                         .setStiffness(250)
                         .setValues(values)
-                        .build(rv, ADJACENT_PAGE_HORIZONTAL_OFFSET);
+                        .build(rv, index == INDEX_RECENTS_ATTACHED_ALPHA_ANIM
+                                ? RUNNING_TASK_ATTACH_ALPHA : ADJACENT_PAGE_HORIZONTAL_OFFSET);
             }
             default:
                 return super.createStateElementAnimation(index, values);
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/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
deleted file mode 100644
index 8b87718..0000000
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ /dev/null
@@ -1,444 +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.views;
-
-import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
-
-import static com.android.launcher3.Utilities.prefixTextWithIcon;
-import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR;
-
-import android.app.ActivityOptions;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.AppUsageLimit;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.icu.text.MeasureFormat;
-import android.icu.text.MeasureFormat.FormatWidth;
-import android.icu.util.Measure;
-import android.icu.util.MeasureUnit;
-import android.os.UserHandle;
-import android.util.Log;
-import android.util.Pair;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.time.Duration;
-import java.util.Locale;
-
-public final class DigitalWellBeingToast {
-
-    private static final float THRESHOLD_LEFT_ICON_ONLY = 0.4f;
-    private static final float THRESHOLD_RIGHT_ICON_ONLY = 0.6f;
-
-    /** Will span entire width of taskView with full text */
-    private static final int SPLIT_BANNER_FULLSCREEN = 0;
-    /** Used for grid task view, only showing icon and time */
-    private static final int SPLIT_GRID_BANNER_LARGE = 1;
-    /** Used for grid task view, only showing icon */
-    private static final int SPLIT_GRID_BANNER_SMALL = 2;
-
-    @IntDef(value = {
-            SPLIT_BANNER_FULLSCREEN,
-            SPLIT_GRID_BANNER_LARGE,
-            SPLIT_GRID_BANNER_SMALL,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    @interface SplitBannerConfig {
-    }
-
-    static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
-    static final int MINUTE_MS = 60000;
-
-    private static final String TAG = "DigitalWellBeingToast";
-
-    private final RecentsViewContainer mContainer;
-    private final TaskView mTaskView;
-    private final LauncherApps mLauncherApps;
-
-    private final int mBannerHeight;
-
-    private Task mTask;
-    private boolean mHasLimit;
-
-    private long mAppRemainingTimeMs;
-    @Nullable
-    private View mBanner;
-    private ViewOutlineProvider mOldBannerOutlineProvider;
-    private float mBannerOffsetPercentage;
-    @Nullable
-    private SplitBounds mSplitBounds;
-    private float mSplitOffsetTranslationY;
-    private float mSplitOffsetTranslationX;
-
-    private boolean mIsDestroyed = false;
-
-    public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) {
-        mContainer = container;
-        mTaskView = taskView;
-        mLauncherApps = container.asContext().getSystemService(LauncherApps.class);
-        mBannerHeight = container.asContext().getResources().getDimensionPixelSize(
-                R.dimen.digital_wellbeing_toast_height);
-    }
-
-    private void setNoLimit() {
-        mHasLimit = false;
-        mTaskView.setContentDescription(mTask.titleDescription);
-        replaceBanner(null);
-        mAppRemainingTimeMs = -1;
-    }
-
-    private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
-        mAppRemainingTimeMs = appRemainingTimeMs;
-        mHasLimit = true;
-        TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast,
-                mContainer.asContext(), mTaskView);
-        toast.setText(prefixTextWithIcon(mContainer.asContext(), R.drawable.ic_hourglass_top,
-                getText()));
-        toast.setOnClickListener(this::openAppUsageSettings);
-        replaceBanner(toast);
-
-        mTaskView.setContentDescription(
-                getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
-    }
-
-    public String getText() {
-        return getText(mAppRemainingTimeMs, false /* forContentDesc */);
-    }
-
-    public boolean hasLimit() {
-        return mHasLimit;
-    }
-
-    public void initialize(Task task) {
-        if (mIsDestroyed) {
-            throw new IllegalStateException("Cannot re-initialize a destroyed toast");
-        }
-        mTask = task;
-        ORDERED_BG_EXECUTOR.execute(() -> {
-            AppUsageLimit usageLimit = null;
-            try {
-                usageLimit = mLauncherApps.getAppUsageLimit(
-                        mTask.getTopComponent().getPackageName(),
-                        UserHandle.of(mTask.key.userId));
-            } catch (Exception e) {
-                Log.e(TAG, "Error initializing digital well being toast", e);
-            }
-            final long appUsageLimitTimeMs =
-                    usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
-            final long appRemainingTimeMs =
-                    usageLimit != null ? usageLimit.getUsageRemaining() : -1;
-
-            mTaskView.post(() -> {
-                if (mIsDestroyed) {
-                    return;
-                }
-                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
-                    setNoLimit();
-                } else {
-                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
-                }
-            });
-        });
-    }
-
-    /**
-     * Mark the DWB toast as destroyed and remove banner from TaskView.
-     */
-    public void destroy() {
-        mIsDestroyed = true;
-        mTaskView.post(() -> replaceBanner(null));
-    }
-
-    public void setSplitBounds(@Nullable SplitBounds splitBounds) {
-        mSplitBounds = splitBounds;
-    }
-
-    private @SplitBannerConfig int getSplitBannerConfig() {
-        if (mSplitBounds == null
-                || !mContainer.getDeviceProfile().isTablet
-                || mTaskView.isLargeTile()) {
-            return SPLIT_BANNER_FULLSCREEN;
-        }
-
-        // For portrait grid only height of task changes, not width. So we keep the text the same
-        if (!mContainer.getDeviceProfile().isLeftRightSplit) {
-            return SPLIT_GRID_BANNER_LARGE;
-        }
-
-        // For landscape grid, for 30% width we only show icon, otherwise show icon and time
-        if (mTask.key.id == mSplitBounds.leftTopTaskId) {
-            return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY
-                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
-        } else {
-            return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY
-                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
-        }
-    }
-
-    private String getReadableDuration(
-            Duration duration,
-            @StringRes int durationLessThanOneMinuteStringId) {
-        int hours = Math.toIntExact(duration.toHours());
-        int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
-
-        // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
-        if (hours > 0 && minutes > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW)
-                    .formatMeasures(
-                            new Measure(hours, MeasureUnit.HOUR),
-                            new Measure(minutes, MeasureUnit.MINUTE));
-        }
-
-        // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
-        if (hours > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                    new Measure(hours, MeasureUnit.HOUR));
-        }
-
-        // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
-        if (minutes > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                    new Measure(minutes, MeasureUnit.MINUTE));
-        }
-
-        // Use a specific string for usage less than one minute but non-zero.
-        if (duration.compareTo(Duration.ZERO) > 0) {
-            return mContainer.asContext().getString(durationLessThanOneMinuteStringId);
-        }
-
-        // Otherwise, return 0-minute string.
-        return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                new Measure(0, MeasureUnit.MINUTE));
-    }
-
-    /**
-     * Returns text to show for the banner depending on {@link #getSplitBannerConfig()}
-     * If {@param forContentDesc} is {@code true}, this will always return the full
-     * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN}
-     */
-    private String getText(long remainingTime, boolean forContentDesc) {
-        final Duration duration = Duration.ofMillis(
-                remainingTime > MINUTE_MS ?
-                        (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
-                        remainingTime);
-        String readableDuration = getReadableDuration(duration,
-                R.string.shorter_duration_less_than_one_minute
-                /* forceFormatWidth */);
-        @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig();
-        if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
-            return mContainer.asContext().getString(
-                    R.string.time_left_for_app,
-                    readableDuration);
-        }
-
-        if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
-            // show no text
-            return "";
-        } else { // SPLIT_GRID_BANNER_LARGE
-            // only show time
-            return readableDuration;
-        }
-    }
-
-    public void openAppUsageSettings(View view) {
-        final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
-                .putExtra(Intent.EXTRA_PACKAGE_NAME,
-                        mTask.getTopComponent().getPackageName()).addFlags(
-                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        try {
-            final RecentsViewContainer container =
-                    RecentsViewContainer.containerFromContext(view.getContext());
-            final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
-                    view, 0, 0,
-                    view.getWidth(), view.getHeight());
-            container.asContext().startActivity(intent, options.toBundle());
-
-            // TODO: add WW logging on the app usage settings click.
-        } catch (ActivityNotFoundException e) {
-            Log.e(TAG, "Failed to open app usage settings for task "
-                    + mTask.getTopComponent().getPackageName(), e);
-        }
-    }
-
-    private String getContentDescriptionForTask(
-            Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
-        return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
-                mContainer.asContext().getString(
-                        R.string.task_contents_description_with_remaining_time,
-                        task.titleDescription,
-                        getText(appRemainingTimeMs, true /* forContentDesc */)) :
-                task.titleDescription;
-    }
-
-    private void replaceBanner(@Nullable View view) {
-        resetOldBanner();
-        setBanner(view);
-    }
-
-    private void resetOldBanner() {
-        if (mBanner != null) {
-            mBanner.setOutlineProvider(mOldBannerOutlineProvider);
-            mTaskView.removeView(mBanner);
-            mBanner.setOnClickListener(null);
-            mContainer.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner);
-        }
-    }
-
-    private void setBanner(@Nullable View view) {
-        mBanner = view;
-        if (mBanner != null && mTaskView.getRecentsView() != null) {
-            setupAndAddBanner();
-            setBannerOutline();
-        }
-    }
-
-    private void setupAndAddBanner() {
-        FrameLayout.LayoutParams layoutParams =
-                (FrameLayout.LayoutParams) mBanner.getLayoutParams();
-        DeviceProfile deviceProfile = mContainer.getDeviceProfile();
-        layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
-                mTaskView.getFirstSnapshotView().getLayoutParams()).bottomMargin;
-        RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
-        Pair<Float, Float> translations = orientationHandler
-                .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
-                        mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
-                        mTaskView.getSnapshotViews(), mTask.key.id, mBanner);
-        mSplitOffsetTranslationX = translations.first;
-        mSplitOffsetTranslationY = translations.second;
-        updateTranslationY();
-        updateTranslationX();
-        mTaskView.addView(mBanner);
-    }
-
-    private void setBannerOutline() {
-        // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
-        mOldBannerOutlineProvider = mBanner.getOutlineProvider() != null
-                ? mBanner.getOutlineProvider()
-                : ViewOutlineProvider.BACKGROUND;
-
-        mBanner.setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                mOldBannerOutlineProvider.getOutline(view, outline);
-                float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY;
-                outline.offset(0, Math.round(verticalTranslation));
-            }
-        });
-        mBanner.setClipToOutline(true);
-    }
-
-    void updateBannerOffset(float offsetPercentage) {
-        if (mBannerOffsetPercentage != offsetPercentage) {
-            mBannerOffsetPercentage = offsetPercentage;
-            if (mBanner != null) {
-                updateTranslationY();
-                mBanner.invalidateOutline();
-            }
-        }
-    }
-
-    private void updateTranslationY() {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setTranslationY(
-                (mBannerOffsetPercentage * mBannerHeight) + mSplitOffsetTranslationY);
-    }
-
-    private void updateTranslationX() {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setTranslationX(mSplitOffsetTranslationX);
-    }
-
-    void setBannerColorTint(int color, float amount) {
-        if (mBanner == null) {
-            return;
-        }
-        if (amount == 0) {
-            mBanner.setLayerType(View.LAYER_TYPE_NONE, null);
-        }
-        Paint layerPaint = new Paint();
-        layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
-        mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint);
-        mBanner.setLayerPaint(layerPaint);
-    }
-
-    void setBannerVisibility(int visibility) {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setVisibility(visibility);
-    }
-
-    private int getAccessibilityActionId() {
-        return (mSplitBounds != null
-                && mSplitBounds.rightBottomTaskId == mTask.key.id)
-                ? R.id.action_digital_wellbeing_bottom_right
-                : R.id.action_digital_wellbeing_top_left;
-    }
-
-    @Nullable
-    public AccessibilityNodeInfo.AccessibilityAction getDWBAccessibilityAction() {
-        if (!hasLimit()) {
-            return null;
-        }
-
-        Context context = mContainer.asContext();
-        String label =
-                (mTaskView.containsMultipleTasks())
-                        ? context.getString(
-                        R.string.split_app_usage_settings,
-                        TaskUtils.getTitle(context, mTask)
-                ) : context.getString(R.string.accessibility_app_usage_settings);
-        return new AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label);
-    }
-
-    public boolean handleAccessibilityAction(int action) {
-        if (getAccessibilityActionId() == action) {
-            openAppUsageSettings(mTaskView);
-            return true;
-        } else {
-            return false;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
new file mode 100644
index 0000000..731b008
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -0,0 +1,399 @@
+/*
+ * 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.views
+
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.AppUsageLimit
+import android.graphics.Outline
+import android.graphics.Paint
+import android.icu.text.MeasureFormat
+import android.icu.util.Measure
+import android.icu.util.MeasureUnit
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
+import android.view.ViewOutlineProvider
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.core.util.component1
+import androidx.core.util.component2
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.quickstep.TaskUtils
+import com.android.systemui.shared.recents.model.Task
+import java.time.Duration
+import java.util.Locale
+
+class DigitalWellBeingToast(
+    private val container: RecentsViewContainer,
+    private val taskView: TaskView
+) {
+    private val launcherApps: LauncherApps? =
+        container.asContext().getSystemService(LauncherApps::class.java)
+
+    private val bannerHeight =
+        container
+            .asContext()
+            .resources
+            .getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
+
+    private lateinit var task: Task
+
+    private var appRemainingTimeMs: Long = 0
+    private var banner: View? = null
+    private var oldBannerOutlineProvider: ViewOutlineProvider? = null
+    private var splitOffsetTranslationY = 0f
+    private var splitOffsetTranslationX = 0f
+
+    private var isDestroyed = false
+
+    var hasLimit = false
+    var splitBounds: SplitConfigurationOptions.SplitBounds? = null
+    var bannerOffsetPercentage = 0f
+        set(value) {
+            if (field != value) {
+                field = value
+                banner?.let {
+                    updateTranslationY()
+                    it.invalidateOutline()
+                }
+            }
+        }
+
+    private fun setNoLimit() {
+        hasLimit = false
+        taskView.contentDescription = task.titleDescription
+        replaceBanner(null)
+        appRemainingTimeMs = -1
+    }
+
+    private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
+        this.appRemainingTimeMs = appRemainingTimeMs
+        hasLimit = true
+        val toast =
+            container.viewCache
+                .getView<TextView>(
+                    R.layout.digital_wellbeing_toast,
+                    container.asContext(),
+                    taskView
+                )
+                .apply {
+                    text =
+                        Utilities.prefixTextWithIcon(
+                            container.asContext(),
+                            R.drawable.ic_hourglass_top,
+                            getBannerText()
+                        )
+                    setOnClickListener(::openAppUsageSettings)
+                }
+        replaceBanner(toast)
+
+        taskView.contentDescription =
+            getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
+    }
+
+    fun initialize(task: Task) {
+        check(!isDestroyed) { "Cannot re-initialize a destroyed toast" }
+        this.task = task
+        Executors.ORDERED_BG_EXECUTOR.execute {
+            var usageLimit: AppUsageLimit? = null
+            try {
+                usageLimit =
+                    launcherApps?.getAppUsageLimit(
+                        this.task.topComponent.packageName,
+                        UserHandle.of(this.task.key.userId)
+                    )
+            } catch (e: Exception) {
+                Log.e(TAG, "Error initializing digital well being toast", e)
+            }
+            val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1
+            val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1
+            taskView.post {
+                if (isDestroyed) {
+                    return@post
+                }
+                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+                    setNoLimit()
+                } else {
+                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs)
+                }
+            }
+        }
+    }
+
+    /** Mark the DWB toast as destroyed and remove banner from TaskView. */
+    fun destroy() {
+        isDestroyed = true
+        taskView.post { replaceBanner(null) }
+    }
+
+    private fun getSplitBannerConfig(): SplitBannerConfig {
+        val splitBounds = splitBounds
+        return when {
+            splitBounds == null || !container.deviceProfile.isTablet || taskView.isLargeTile ->
+                SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
+            // For portrait grid only height of task changes, not width. So we keep the text the
+            // same
+            !container.deviceProfile.isLeftRightSplit -> SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+            // For landscape grid, for 30% width we only show icon, otherwise show icon and time
+            task.key.id == splitBounds.leftTopTaskId ->
+                if (splitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
+                    SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+                else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+            else ->
+                if (splitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY)
+                    SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+                else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+        }
+    }
+
+    private fun getReadableDuration(
+        duration: Duration,
+        @StringRes durationLessThanOneMinuteStringId: Int
+    ): String {
+        val hours = Math.toIntExact(duration.toHours())
+        val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes())
+        return when {
+            // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
+            hours > 0 && minutes > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW)
+                    .formatMeasures(
+                        Measure(hours, MeasureUnit.HOUR),
+                        Measure(minutes, MeasureUnit.MINUTE)
+                    )
+            // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
+            hours > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(hours, MeasureUnit.HOUR))
+            // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
+            minutes > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(minutes, MeasureUnit.MINUTE))
+            // Use a specific string for usage less than one minute but non-zero.
+            duration > Duration.ZERO ->
+                container.asContext().getString(durationLessThanOneMinuteStringId)
+            // Otherwise, return 0-minute string.
+            else ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(0, MeasureUnit.MINUTE))
+        }
+    }
+
+    /**
+     * Returns text to show for the banner depending on [.getSplitBannerConfig] If {@param
+     * forContentDesc} is `true`, this will always return the full string corresponding to
+     * [.SPLIT_BANNER_FULLSCREEN]
+     */
+    @JvmOverloads
+    fun getBannerText(
+        remainingTime: Long = appRemainingTimeMs,
+        forContentDesc: Boolean = false
+    ): String {
+        val duration =
+            Duration.ofMillis(
+                if (remainingTime > MINUTE_MS)
+                    (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS
+                else remainingTime
+            )
+        val readableDuration =
+            getReadableDuration(
+                duration,
+                R.string.shorter_duration_less_than_one_minute /* forceFormatWidth */
+            )
+        val splitBannerConfig = getSplitBannerConfig()
+        return when {
+            forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN ->
+                container.asContext().getString(R.string.time_left_for_app, readableDuration)
+            // show no text
+            splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> ""
+            // SPLIT_GRID_BANNER_LARGE only show time
+            else -> readableDuration
+        }
+    }
+
+    private fun openAppUsageSettings(view: View) {
+        val intent =
+            Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, task.topComponent.packageName)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        try {
+            val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
+            container.asContext().startActivity(intent, options.toBundle())
+
+            // TODO: add WW logging on the app usage settings click.
+        } catch (e: ActivityNotFoundException) {
+            Log.e(
+                TAG,
+                "Failed to open app usage settings for task " + task.topComponent.packageName,
+                e
+            )
+        }
+    }
+
+    private fun getContentDescriptionForTask(
+        task: Task,
+        appUsageLimitTimeMs: Long,
+        appRemainingTimeMs: Long
+    ): String =
+        if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0)
+            container
+                .asContext()
+                .getString(
+                    R.string.task_contents_description_with_remaining_time,
+                    task.titleDescription,
+                    getBannerText(appRemainingTimeMs, true /* forContentDesc */)
+                )
+        else task.titleDescription
+
+    private fun replaceBanner(view: View?) {
+        resetOldBanner()
+        setBanner(view)
+    }
+
+    private fun resetOldBanner() {
+        val banner = banner ?: return
+        banner.outlineProvider = oldBannerOutlineProvider
+        taskView.removeView(banner)
+        banner.setOnClickListener(null)
+        container.viewCache.recycleView(R.layout.digital_wellbeing_toast, banner)
+    }
+
+    private fun setBanner(banner: View?) {
+        this.banner = banner
+        if (banner != null && taskView.recentsView != null) {
+            setupAndAddBanner()
+            setBannerOutline()
+        }
+    }
+
+    private fun setupAndAddBanner() {
+        val banner = banner ?: return
+        banner.updateLayoutParams<FrameLayout.LayoutParams> {
+            bottomMargin =
+                (taskView.firstSnapshotView.layoutParams as MarginLayoutParams).bottomMargin
+        }
+        val (translationX, translationY) =
+            taskView.pagedOrientationHandler.getDwbLayoutTranslations(
+                taskView.measuredWidth,
+                taskView.measuredHeight,
+                splitBounds,
+                container.deviceProfile,
+                taskView.snapshotViews,
+                task.key.id,
+                banner
+            )
+        splitOffsetTranslationX = translationX
+        splitOffsetTranslationY = translationY
+        updateTranslationY()
+        updateTranslationX()
+        taskView.addView(banner)
+    }
+
+    private fun setBannerOutline() {
+        val banner = banner ?: return
+        // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
+        val oldBannerOutlineProvider =
+            if (banner.outlineProvider != null) banner.outlineProvider
+            else ViewOutlineProvider.BACKGROUND
+        this.oldBannerOutlineProvider = oldBannerOutlineProvider
+
+        banner.outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    oldBannerOutlineProvider.getOutline(view, outline)
+                    val verticalTranslation = -view.translationY + splitOffsetTranslationY
+                    outline.offset(0, Math.round(verticalTranslation))
+                }
+            }
+        banner.clipToOutline = true
+    }
+
+    private fun updateTranslationY() {
+        banner?.translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
+    }
+
+    private fun updateTranslationX() {
+        banner?.translationX = splitOffsetTranslationX
+    }
+
+    fun setBannerColorTint(color: Int, amount: Float) {
+        val banner = banner ?: return
+        if (amount == 0f) {
+            banner.setLayerType(View.LAYER_TYPE_NONE, null)
+        }
+        val layerPaint = Paint()
+        layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount))
+        banner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
+        banner.setLayerPaint(layerPaint)
+    }
+
+    fun setBannerVisibility(visibility: Int) {
+        banner?.visibility = visibility
+    }
+
+    private fun getAccessibilityActionId(): Int =
+        if (splitBounds?.rightBottomTaskId == task.key.id)
+            R.id.action_digital_wellbeing_bottom_right
+        else R.id.action_digital_wellbeing_top_left
+
+    fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? {
+        if (!hasLimit) return null
+        val context = container.asContext()
+        val label =
+            if ((taskView.containsMultipleTasks()))
+                context.getString(
+                    R.string.split_app_usage_settings,
+                    TaskUtils.getTitle(context, task)
+                )
+            else context.getString(R.string.accessibility_app_usage_settings)
+        return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label)
+    }
+
+    fun handleAccessibilityAction(action: Int): Boolean {
+        if (getAccessibilityActionId() != action) return false
+        openAppUsageSettings(taskView)
+        return true
+    }
+
+    companion object {
+        private const val THRESHOLD_LEFT_ICON_ONLY = 0.4f
+        private const val THRESHOLD_RIGHT_ICON_ONLY = 0.6f
+
+        enum class SplitBannerConfig {
+            /** Will span entire width of taskView with full text */
+            SPLIT_BANNER_FULLSCREEN,
+            /** Used for grid task view, only showing icon and time */
+            SPLIT_GRID_BANNER_LARGE,
+            /** Used for grid task view, only showing icon */
+            SPLIT_GRID_BANNER_SMALL
+        }
+
+        val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS)
+        const val MINUTE_MS: Int = 60000
+
+        private const val TAG = "DigitalWellBeingToast"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index a046c42..ba4d786 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -130,7 +130,7 @@
         taskContainers.forEach { it.bind() }
 
         this.splitBoundsConfig = splitBoundsConfig
-        taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) }
+        taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig }
         setOrientationState(orientedState)
     }
 
@@ -210,7 +210,7 @@
     fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) {
         splitBoundsConfig = splitBounds
         taskContainers.forEach {
-            it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig)
+            it.digitalWellBeingToast?.splitBounds = splitBoundsConfig
             it.digitalWellBeingToast?.initialize(it.task)
         }
         invalidate()
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index e3c52d7..69a9690 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -36,6 +36,7 @@
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
+import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation;
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
@@ -231,9 +232,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;
 
@@ -320,6 +321,27 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> RUNNING_TASK_ATTACH_ALPHA =
+            new FloatProperty<RecentsView>("runningTaskAttachAlpha") {
+                @Override
+                public void setValue(RecentsView recentsView, float v) {
+                    TaskView runningTask = recentsView.getRunningTaskView();
+                    if (runningTask == null) {
+                        return;
+                    }
+                    runningTask.setAttachAlpha(v);
+                }
+
+                @Override
+                public Float get(RecentsView recentsView) {
+                    TaskView runningTask = recentsView.getRunningTaskView();
+                    if (runningTask == null) {
+                        return null;
+                    }
+                    return runningTask.getAttachAlpha();
+                }
+            };
+
     public static final int SCROLL_VIBRATION_PRIMITIVE =
             Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1;
     public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f;
@@ -2737,10 +2759,17 @@
         showCurrentTask(mActiveGestureRunningTasks);
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
-        setRunningTaskHidden(true);
+        setRunningTaskHidden(!shouldUpdateRunningTaskAlpha());
         setTaskIconScaledDown(true);
     }
 
+    /**
+     * Returns whether the running task's attach alpha should be updated during the attach animation
+     */
+    public boolean shouldUpdateRunningTaskAlpha() {
+        return enableDesktopTaskAlphaAnimation() && getRunningTaskView() instanceof DesktopTaskView;
+    }
+
     private boolean isGestureActive() {
         return mActiveGestureRunningTasks != null;
     }
@@ -3021,12 +3050,13 @@
     public void setRunningTaskHidden(boolean isHidden) {
         mRunningTaskTileHidden = isHidden;
         TaskView runningTask = getRunningTaskView();
-        if (runningTask != null) {
-            runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
-            if (!isHidden) {
-                AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
-                        AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
-            }
+        if (runningTask == null) {
+            return;
+        }
+        runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
+        if (!isHidden) {
+            AccessibilityManagerCompat.sendCustomAccessibilityEvent(
+                    runningTask, AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
         }
     }
 
@@ -3273,12 +3303,12 @@
             longRowWidth = largeTaskWidthAndSpacing;
         }
 
-        if (longRowWidth < mLastComputedGridSize.width()) {
-            mClearAllShortTotalWidthTranslation =
-                    (mIsRtl
-                            ? mLastComputedTaskSize.right
-                            : deviceProfile.widthPx - mLastComputedTaskSize.left)
-                            - longRowWidth - deviceProfile.overviewGridSideMargin;
+        // 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) {
@@ -4701,8 +4731,11 @@
                     : showAsGrid
                             ? gridOffsetSize
                             : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize;
-            float totalTranslationX = translation + modalTranslation;
             View child = getChildAt(i);
+            boolean skipTranslationOffset = enableDesktopTaskAlphaAnimation()
+                    && i == getRunningTaskIndex()
+                    && child instanceof DesktopTaskView;
+            float totalTranslationX = (skipTranslationOffset ? 0f : translation) + modalTranslation;
             FloatProperty translationPropertyX = child instanceof TaskView
                     ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
                     : getPagedOrientationHandler().getPrimaryViewTranslate();
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index e43d7b4..815f8fa 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -65,6 +65,7 @@
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.MultiPropertyFactory
 import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
+import com.android.launcher3.util.MultiValueAlpha
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SafeCloseable
 import com.android.launcher3.util.SplitConfigurationOptions
@@ -391,11 +392,19 @@
             applyTranslationX()
         }
 
-    protected var stableAlpha = 1f
+    private val taskViewAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+
+    protected var stableAlpha
         set(value) {
-            field = value
-            alpha = stableAlpha
+            taskViewAlpha.get(ALPHA_INDEX_STABLE).value = value
         }
+        get() = taskViewAlpha.get(ALPHA_INDEX_STABLE).value
+
+    protected var attachAlpha
+        set(value) {
+            taskViewAlpha.get(ALPHA_INDEX_ATTACH).value = value
+        }
+        get() = taskViewAlpha.get(ALPHA_INDEX_ATTACH).value
 
     protected var shouldShowScreenshot = false
         get() = !isRunningTask || field
@@ -1398,7 +1407,7 @@
     private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) {
         taskContainers.forEach {
             it.iconView.setContentAlpha(focusTransitionProgress)
-            it.digitalWellBeingToast?.updateBannerOffset(1f - focusTransitionProgress)
+            it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - focusTransitionProgress
         }
     }
 
@@ -1548,7 +1557,7 @@
     private fun onModalnessUpdated(modalness: Float) {
         taskContainers.forEach {
             it.iconView.setModalAlpha(1 - modalness)
-            it.digitalWellBeingToast?.updateBannerOffset(modalness)
+            it.digitalWellBeingToast?.bannerOffsetPercentage = modalness
         }
     }
 
@@ -1584,7 +1593,7 @@
         }
         dismissScale = 1f
         translationZ = 0f
-        alpha = stableAlpha
+        attachAlpha = 1f
         setIconScaleAndDim(1f)
         setColorTint(0f, 0)
     }
@@ -1661,6 +1670,11 @@
         const val FOCUS_TRANSITION_INDEX_SCALE_AND_DIM = 1
         const val FOCUS_TRANSITION_INDEX_COUNT = 2
 
+        private const val ALPHA_INDEX_STABLE = 0
+        private const val ALPHA_INDEX_ATTACH = 1
+
+        private const val NUM_ALPHA_CHANNELS = 2
+
         /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
         const val MAX_PAGE_SCRIM_ALPHA = 0.4f
         const val SCALE_ICON_DURATION: Long = 120
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/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/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
index 6e25b10..e981570 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
@@ -67,15 +67,15 @@
             mLauncher.goHome();
             final DigitalWellBeingToast toast = getToast();
 
-            waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit());
-            assertEquals("Toast text: ", "5 minutes left today", toast.getText());
+            waitForLauncherCondition("Toast is not visible", launcher -> toast.getHasLimit());
+            assertEquals("Toast text: ", "5 minutes left today", toast.getBannerText());
 
             // Unset time limit for app.
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
 
             mLauncher.goHome();
-            assertFalse("Toast is visible", getToast().hasLimit());
+            assertFalse("Toast is visible", getToast().getHasLimit());
         } finally {
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
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/layout/launcher.xml b/res/layout/launcher.xml
index a709fbc..83c8d6c 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -51,7 +51,7 @@
 
         <!-- Keep these behind the workspace so that they are not visible when
          we go into AllApps -->
-        <com.android.launcher3.pageindicators.WorkspacePageIndicator
+        <com.android.launcher3.pageindicators.PageIndicatorDots
             android:id="@+id/page_indicator"
             android:layout_width="match_parent"
             android:layout_height="@dimen/workspace_page_indicator_height"
diff --git a/res/layout/page_indicator_dots.xml b/res/layout/page_indicator_dots.xml
deleted file mode 100644
index d5fe51e..0000000
--- a/res/layout/page_indicator_dots.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2022 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.pageindicators.PageIndicatorDots xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/page_indicator"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/workspace_page_indicator_height"
-    android:layout_gravity="bottom | center_horizontal"
-    android:theme="@style/HomeScreenElementTheme" />
\ No newline at end of file
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index a054450..4f5d111 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -31,7 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Écran partagé"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Infos sur l\'appli pour %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Paramètres d\'utilisation pour %1$s"</string>
-    <string name="save_app_pair" msgid="5647523853662686243">"Enregistrer la paire d\'applis"</string>
+    <string name="save_app_pair" msgid="5647523853662686243">"Enregistrer une paire d\'applis"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applications n\'est pas prise en charge sur cet appareil"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Dépliez l\'appareil pour utiliser cette paire d\'applications"</string>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3b239ac..f8c075f 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.
@@ -306,7 +311,7 @@
     <dimen name="blur_size_medium_outline">2dp</dimen>
     <dimen name="blur_size_click_shadow">4dp</dimen>
     <dimen name="click_shadow_high_shift">2dp</dimen>
-    <dimen name="app_title_icon_shadow_inset">1dp</dimen>
+    <dimen name="app_title_icon_shadow_inset">0.5dp</dimen>
 
     <!-- Pending widget -->
     <dimen name="pending_widget_min_padding">8dp</dimen>
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/Launcher.java b/src/com/android/launcher3/Launcher.java
index 9f122c1..3ca6099 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -135,14 +135,12 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.text.method.TextKeyListener;
-import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MotionEvent;
 import android.view.View;
@@ -214,7 +212,6 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupDataProvider;
@@ -1410,15 +1407,6 @@
                 this, R.attr.isWorkspaceDarkText) ? Color.BLACK : Color.WHITE);
     }
 
-    @Override
-    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
-        if (WorkspacePageIndicator.class.getName().equals(name)) {
-            return LayoutInflater.from(context).inflate(R.layout.page_indicator_dots,
-                    (ViewGroup) parent, false);
-        }
-        return super.onCreateView(parent, name, context, attrs);
-    }
-
     /**
      * Add a shortcut to the workspace or to a Folder.
      *
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/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/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
index b12b2bc..2ee5b80 100644
--- a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
+++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
@@ -38,6 +38,7 @@
     LauncherApps.Callback() {
 
     override fun onPackageAdded(packageName: String, user: UserHandle) {
+        FileLog.d(TAG, "onPackageAdded triggered for packageName=$packageName, user=$user")
         taskExecutor.accept(PackageUpdatedTask(OP_ADD, user, packageName))
     }
 
@@ -54,7 +55,7 @@
     }
 
     override fun onPackageRemoved(packageName: String, user: UserHandle) {
-        FileLog.d(TAG, "package removed received $packageName")
+        FileLog.d(TAG, "onPackageRemoved triggered for packageName=$packageName, user=$user")
         taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, packageName))
     }
 
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
deleted file mode 100644
index bde4e52..0000000
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ /dev/null
@@ -1,265 +0,0 @@
-package com.android.launcher3.pageindicators;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.AttributeSet;
-import android.util.Property;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Themes;
-
-/**
- * A PageIndicator that briefly shows a fraction of a line when moving between pages
- *
- * The fraction is 1 / number of pages and the position is based on the progress of the page scroll.
- */
-public class WorkspacePageIndicator extends View implements Insettable, PageIndicator {
-
-    private static final int LINE_ANIMATE_DURATION = ViewConfiguration.getScrollBarFadeDuration();
-    private static final int LINE_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay();
-    public static final int WHITE_ALPHA = (int) (0.70f * 255);
-    public static final int BLACK_ALPHA = (int) (0.65f * 255);
-
-    private static final int LINE_ALPHA_ANIMATOR_INDEX = 0;
-    private static final int NUM_PAGES_ANIMATOR_INDEX = 1;
-    private static final int TOTAL_SCROLL_ANIMATOR_INDEX = 2;
-    private static final int ANIMATOR_COUNT = 3;
-
-    private ValueAnimator[] mAnimators = new ValueAnimator[ANIMATOR_COUNT];
-
-    private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper());
-    private final Launcher mLauncher;
-
-    private boolean mShouldAutoHide = true;
-
-    // The alpha of the line when it is showing.
-    private int mActiveAlpha = 0;
-    // The alpha that the line is being animated to or already at (either 0 or mActiveAlpha).
-    private int mToAlpha;
-    // A float value representing the number of pages, to allow for an animation when it changes.
-    private float mNumPagesFloat;
-    private int mCurrentScroll;
-    private int mTotalScroll;
-    private Paint mLinePaint;
-    private final int mLineHeight;
-
-    private static final Property<WorkspacePageIndicator, Integer> PAINT_ALPHA
-            = new Property<WorkspacePageIndicator, Integer>(Integer.class, "paint_alpha") {
-        @Override
-        public Integer get(WorkspacePageIndicator obj) {
-            return obj.mLinePaint.getAlpha();
-        }
-
-        @Override
-        public void set(WorkspacePageIndicator obj, Integer alpha) {
-            obj.mLinePaint.setAlpha(alpha);
-            obj.invalidate();
-        }
-    };
-
-    private static final Property<WorkspacePageIndicator, Float> NUM_PAGES
-            = new Property<WorkspacePageIndicator, Float>(Float.class, "num_pages") {
-        @Override
-        public Float get(WorkspacePageIndicator obj) {
-            return obj.mNumPagesFloat;
-        }
-
-        @Override
-        public void set(WorkspacePageIndicator obj, Float numPages) {
-            obj.mNumPagesFloat = numPages;
-            obj.invalidate();
-        }
-    };
-
-    private static final Property<WorkspacePageIndicator, Integer> TOTAL_SCROLL
-            = new Property<WorkspacePageIndicator, Integer>(Integer.class, "total_scroll") {
-        @Override
-        public Integer get(WorkspacePageIndicator obj) {
-            return obj.mTotalScroll;
-        }
-
-        @Override
-        public void set(WorkspacePageIndicator obj, Integer totalScroll) {
-            obj.mTotalScroll = totalScroll;
-            obj.invalidate();
-        }
-    };
-
-    private Runnable mHideLineRunnable = () -> animateLineToAlpha(0);
-
-    public WorkspacePageIndicator(Context context) {
-        this(context, null);
-    }
-
-    public WorkspacePageIndicator(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        Resources res = context.getResources();
-        mLinePaint = new Paint();
-        mLinePaint.setAlpha(0);
-
-        mLauncher = Launcher.getLauncher(context);
-        mLineHeight = res.getDimensionPixelSize(R.dimen.workspace_page_indicator_line_height);
-
-        boolean darkText = Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText);
-        mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA;
-        mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE);
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        if (mTotalScroll == 0 || mNumPagesFloat == 0) {
-            return;
-        }
-
-        // Compute and draw line rect.
-        float progress = Utilities.boundToRange(((float) mCurrentScroll) / mTotalScroll, 0f, 1f);
-        int availableWidth = getWidth();
-        int lineWidth = (int) (availableWidth / mNumPagesFloat);
-        int lineLeft = (int) (progress * (availableWidth - lineWidth));
-        int lineRight = lineLeft + lineWidth;
-
-        canvas.drawRoundRect(lineLeft, getHeight() / 2 - mLineHeight / 2, lineRight,
-                getHeight() / 2 + mLineHeight / 2, mLineHeight, mLineHeight, mLinePaint);
-    }
-
-    @Override
-    public void setScroll(int currentScroll, int totalScroll) {
-        if (getAlpha() == 0) {
-            return;
-        }
-        animateLineToAlpha(mActiveAlpha);
-
-        mCurrentScroll = currentScroll;
-        if (mTotalScroll == 0) {
-            mTotalScroll = totalScroll;
-        } else if (mTotalScroll != totalScroll) {
-            animateToTotalScroll(totalScroll);
-        } else {
-            invalidate();
-        }
-
-        if (mShouldAutoHide) {
-            hideAfterDelay();
-        }
-    }
-
-    private void hideAfterDelay() {
-        mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
-        mDelayedLineFadeHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY);
-    }
-
-    @Override
-    public void setActiveMarker(int activePage) { }
-
-    @Override
-    public void setMarkersCount(int numMarkers) {
-        if (Float.compare(numMarkers, mNumPagesFloat) != 0) {
-            setupAndRunAnimation(ObjectAnimator.ofFloat(this, NUM_PAGES, numMarkers),
-                    NUM_PAGES_ANIMATOR_INDEX);
-        } else {
-            if (mAnimators[NUM_PAGES_ANIMATOR_INDEX] != null) {
-                mAnimators[NUM_PAGES_ANIMATOR_INDEX].cancel();
-                mAnimators[NUM_PAGES_ANIMATOR_INDEX] = null;
-            }
-        }
-    }
-
-    @Override
-    public void setShouldAutoHide(boolean shouldAutoHide) {
-        mShouldAutoHide = shouldAutoHide;
-        if (shouldAutoHide && mLinePaint.getAlpha() > 0) {
-            hideAfterDelay();
-        } else if (!shouldAutoHide) {
-            mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
-        }
-    }
-
-    private void animateLineToAlpha(int alpha) {
-        if (alpha == mToAlpha) {
-            // Ignore the new animation if it is going to the same alpha as the current animation.
-            return;
-        }
-        mToAlpha = alpha;
-        setupAndRunAnimation(ObjectAnimator.ofInt(this, PAINT_ALPHA, alpha),
-                LINE_ALPHA_ANIMATOR_INDEX);
-    }
-
-    private void animateToTotalScroll(int totalScroll) {
-        setupAndRunAnimation(ObjectAnimator.ofInt(this, TOTAL_SCROLL, totalScroll),
-                TOTAL_SCROLL_ANIMATOR_INDEX);
-    }
-
-    /**
-     * Starts the given animator and stores it in the provided index in {@link #mAnimators} until
-     * the animation ends.
-     *
-     * If an animator is already at the index (i.e. it is already playing), it is canceled and
-     * replaced with the new animator.
-     */
-    private void setupAndRunAnimation(ValueAnimator animator, final int animatorIndex) {
-        if (mAnimators[animatorIndex] != null) {
-            mAnimators[animatorIndex].cancel();
-        }
-        mAnimators[animatorIndex] = animator;
-        mAnimators[animatorIndex].addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimators[animatorIndex] = null;
-            }
-        });
-        mAnimators[animatorIndex].setDuration(LINE_ANIMATE_DURATION);
-        mAnimators[animatorIndex].start();
-    }
-
-    /**
-     * Pauses all currently running animations.
-     */
-    @Override
-    public void pauseAnimations() {
-        for (int i = 0; i < ANIMATOR_COUNT; i++) {
-            if (mAnimators[i] != null) {
-                mAnimators[i].pause();
-            }
-        }
-    }
-
-    /**
-     * Force-ends all currently running or paused animations.
-     */
-    @Override
-    public void skipAnimationsToEnd() {
-        for (int i = 0; i < ANIMATOR_COUNT; i++) {
-            if (mAnimators[i] != null) {
-                mAnimators[i].end();
-            }
-        }
-    }
-
-    /**
-     * We need to override setInsets to prevent InsettableFrameLayout from applying different
-     * margins on the page indicator.
-     */
-    @Override
-    public void setInsets(Rect insets) {
-    }
-}
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/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/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())
     }
 }