Merge "Converting LauncherPrefs to dagger" into main
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index c50e82d..c2cabd0 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -229,9 +229,7 @@
                     (WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
             if (isPredictedIcon(child) && child.isEnabled()) {
                 PredictedAppIcon icon = (PredictedAppIcon) child;
-                boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem);
-                icon.applyFromWorkspaceItem(predictedItem, animateIconChange, numViewsAnimated);
-                if (animateIconChange) {
+                if (icon.applyFromWorkspaceItemWithAnimation(predictedItem, numViewsAnimated)) {
                     numViewsAnimated++;
                 }
                 icon.finishBinding(mPredictionLongClickListener);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 741853e..6b9f5a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -64,6 +64,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer;
 import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.Themes;
@@ -595,10 +596,12 @@
             // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
             if (hotseatView instanceof BubbleTextView btv
                     && hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) {
-                boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
-                btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
-                if (animate) {
-                    numViewsAnimated++;
+                if (btv instanceof PredictedAppIcon pai) {
+                    if (pai.applyFromWorkspaceItemWithAnimation(workspaceInfo, numViewsAnimated)) {
+                        numViewsAnimated++;
+                    }
+                } else {
+                    btv.applyFromWorkspaceItem(workspaceInfo);
                 }
             }
             setClickAndLongClickListenersForIcon(hotseatView);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index fd4cf0e..0abd88c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -299,7 +299,7 @@
         private final PointF mTouchDownLocation = new PointF();
         private final PointF mViewInitialPosition = new PointF();
         private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
-        private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
+        private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout();
         private State mState = State.IDLE;
         private int mTouchSlop = -1;
         private BubbleDragAnimator mAnimator;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
deleted file mode 100644
index 721c831..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2019 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.uioverrides;
-
-import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
-import static com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT;
-import static com.android.app.animation.Interpolators.FINAL_FRAME;
-import static com.android.app.animation.Interpolators.INSTANT;
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
-import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
-import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS;
-import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
-import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
-import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
-import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
-
-import android.util.FloatProperty;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.StateManager.StateHandler;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * State handler for recents view. Manages UI changes and animations for recents view based off the
- * current {@link LauncherState}.
- *
- * @param <T> the recents view
- */
-public abstract class BaseRecentsViewStateController<T extends RecentsView>
-        implements StateHandler<LauncherState> {
-    protected final T mRecentsView;
-    protected final QuickstepLauncher mLauncher;
-
-    public BaseRecentsViewStateController(@NonNull QuickstepLauncher launcher) {
-        mLauncher = launcher;
-        mRecentsView = launcher.getOverviewPanel();
-    }
-
-    @Override
-    public void setState(@NonNull LauncherState state) {
-        float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
-        RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
-        ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, scaleAndOffset[1]);
-        TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f);
-
-        getContentAlphaProperty().set(mRecentsView, state.isRecentsViewVisible ? 1f : 0);
-        getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
-        RECENTS_GRID_PROGRESS.set(mRecentsView,
-                state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f);
-        TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, state.showTaskThumbnailSplash() ? 1f : 0f);
-        if (enableLargeDesktopWindowingTile()) {
-            DESKTOP_CAROUSEL_DETACH_PROGRESS.set(mRecentsView,
-                    state.detachDesktopCarousel() ? 1f : 0f);
-        }
-    }
-
-    @Override
-    public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
-            PendingAnimation builder) {
-        if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
-            return;
-        }
-        setStateWithAnimationInternal(toState, config, builder);
-        builder.addEndListener(success -> {
-            if (!success && !toState.isRecentsViewVisible) {
-                mRecentsView.reset();
-            }
-        });
-    }
-
-    /**
-     * Core logic for animating the recents view UI.
-     *
-     * @param toState state to animate to
-     * @param config current animation config
-     * @param setter animator set builder
-     */
-    void setStateWithAnimationInternal(@NonNull final LauncherState toState,
-            @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
-        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
-        setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
-                config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
-        setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1],
-                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
-        setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
-                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
-
-        boolean exitingOverview =
-                !FeatureFlags.enableSplitContextually() && !toState.isRecentsViewVisible;
-        if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
-            setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
-                    .createPlaceholderDismissAnim(mLauncher, LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
-                            setter.getDuration()));
-            setter.setViewAlpha(
-                    mRecentsView.getSplitInstructionsView(),
-                    0,
-                    config.getInterpolator(
-                            ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
-                            LINEAR
-                    )
-            );
-        }
-
-        setter.setFloat(mRecentsView, getContentAlphaProperty(),
-                toState.isRecentsViewVisible ? 1 : 0,
-                config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
-
-        setter.setFloat(
-                mRecentsView, getTaskModalnessProperty(),
-                toState.getOverviewModalness(),
-                config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
-
-        LauncherState fromState = mLauncher.getStateManager().getState();
-        setter.setFloat(mRecentsView, TASK_THUMBNAIL_SPLASH_ALPHA,
-                toState.showTaskThumbnailSplash() ? 1f : 0f,
-                getOverviewInterpolator(fromState, toState));
-
-        setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
-                toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f,
-                getOverviewInterpolator(fromState, toState));
-
-        if (enableLargeDesktopWindowingTile()) {
-            setter.setFloat(mRecentsView, DESKTOP_CAROUSEL_DETACH_PROGRESS,
-                    toState.detachDesktopCarousel() ? 1f : 0f,
-                    getOverviewInterpolator(fromState, toState));
-        }
-    }
-
-    private Interpolator getOverviewInterpolator(LauncherState fromState, LauncherState toState) {
-        return fromState == QUICK_SWITCH_FROM_HOME
-                ? ACCELERATE_DECELERATE
-                : toState.isRecentsViewVisible ? INSTANT : FINAL_FRAME;
-    }
-
-    abstract FloatProperty getTaskModalnessProperty();
-
-    /**
-     * Get property for content alpha for the recents view.
-     *
-     * @return the float property for the view's content alpha
-     */
-    abstract FloatProperty getContentAlphaProperty();
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 535ae1c..caac35e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
-import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
 
 import android.animation.Animator;
@@ -26,8 +25,6 @@
 import android.animation.Keyframe;
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.BlurMaskFilter;
 import android.graphics.Canvas;
@@ -37,7 +34,6 @@
 import android.graphics.Path;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.os.Process;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Property;
@@ -48,12 +44,12 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.icons.LauncherIcons;
@@ -64,10 +60,6 @@
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.DoubleShadowBubbleTextView;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
 /**
  * A BubbleTextView with a ring around it's drawable
  */
@@ -105,12 +97,12 @@
     private final BlurMaskFilter mShadowFilter;
 
     private boolean mIsPinned = false;
-    private int mPlateColor;
+    private final AnimColorHolder mPlateColor = new AnimColorHolder();
     boolean mDrawForDrag = false;
 
-    // Used for the "slot-machine" education animation.
-    private List<Drawable> mSlotMachineIcons;
-    private Animator mSlotMachineAnim;
+    // Used for the "slot-machine" animation when prediction changes.
+    private final Rect mSlotIconBound = new Rect(0, 0, getIconSize(), getIconSize());
+    private Drawable mSlotMachineIcon;
     private float mSlotMachineIconTranslationY;
 
     // Used to animate the "ring" around predicted icons
@@ -153,34 +145,26 @@
     @Override
     public void onDraw(Canvas canvas) {
         int count = canvas.save();
-        boolean isSlotMachineAnimRunning = mSlotMachineAnim != null;
+        boolean isSlotMachineAnimRunning = mSlotMachineIcon != null;
         if (!mIsPinned) {
             drawEffect(canvas);
             if (isSlotMachineAnimRunning) {
                 // Clip to to outside of the ring during the slot machine animation.
                 canvas.clipPath(mRingPath);
             }
-            canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
-            canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
+            canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO,
+                    getWidth() * .5f, getHeight() * .5f);
+            if (isSlotMachineAnimRunning) {
+                canvas.translate(0, mSlotMachineIconTranslationY);
+                mSlotMachineIcon.setBounds(mSlotIconBound);
+                mSlotMachineIcon.draw(canvas);
+                canvas.translate(0, getSlotMachineIconPlusSpacingSize());
+            }
         }
-        if (isSlotMachineAnimRunning) {
-            drawSlotMachineIcons(canvas);
-        } else {
-            super.onDraw(canvas);
-        }
+        super.onDraw(canvas);
         canvas.restoreToCount(count);
     }
 
-    private void drawSlotMachineIcons(Canvas canvas) {
-        canvas.translate((getWidth() - getIconSize()) / 2f,
-                (getHeight() - getIconSize()) / 2f + mSlotMachineIconTranslationY);
-        for (Drawable icon : mSlotMachineIcons) {
-            icon.setBounds(0, 0, getIconSize(), getIconSize());
-            icon.draw(canvas);
-            canvas.translate(0, getSlotMachineIconPlusSpacingSize());
-        }
-    }
-
     private float getSlotMachineIconPlusSpacingSize() {
         return getIconSize() + getOutlineOffsetY();
     }
@@ -196,104 +180,88 @@
         mIsDrawingDot = false;
     }
 
-    @Override
-    public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
-        // Create the slot machine animation first, since it uses the current icon to start.
-        Animator slotMachineAnim = animate
-                ? createSlotMachineAnim(Collections.singletonList(info.bitmap), false)
-                : null;
-        super.applyFromWorkspaceItem(info, animate, staggerIndex);
-        int oldPlateColor = mPlateColor;
+    /**
+     * Returns whether the newInfo differs from the current getTag().
+     */
+    private boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
+        boolean changedIcons = getTag() instanceof WorkspaceItemInfo oldInfo
+                && oldInfo.getTargetComponent() != null
+                && newInfo.getTargetComponent() != null
+                && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
+        return changedIcons && isShown();
+    }
 
-        int newPlateColor;
+    @Override
+    public void applyIconAndLabel(ItemInfoWithIcon info) {
+        super.applyIconAndLabel(info);
         if (getIcon().isThemed()) {
-            newPlateColor = getResources().getColor(android.R.color.system_accent1_300);
+            mPlateColor.endColor = getResources().getColor(android.R.color.system_accent1_300);
         } else {
             float[] hctPlateColor = new float[3];
             ColorUtils.colorToM3HCT(mDotParams.appColor, hctPlateColor);
-            newPlateColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
+            mPlateColor.endColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
         }
+        mPlateColor.onUpdate();
+    }
+
+    /**
+     * Tries to apply the icon with animation and returns true if the icon was indeed animated
+     */
+    public boolean applyFromWorkspaceItemWithAnimation(WorkspaceItemInfo info, int staggerIndex) {
+        boolean animate = shouldAnimateIconChange(info);
+        Drawable oldIcon = getIcon();
+        int oldPlateColor = mPlateColor.currentColor;
+        applyFromWorkspaceItem(info, null);
+
+        setContentDescription(
+                mIsPinned ? info.contentDescription :
+                        getContext().getString(R.string.hotseat_prediction_content_description,
+                                info.contentDescription));
 
         if (!animate) {
-            mPlateColor = newPlateColor;
-        }
-        if (mIsPinned) {
-            setContentDescription(info.contentDescription);
+            mPlateColor.startColor = mPlateColor.endColor;
+            mPlateColor.progress.value = 1;
+            mPlateColor.onUpdate();
         } else {
-            setContentDescription(
-                    getContext().getString(R.string.hotseat_prediction_content_description,
-                            info.contentDescription));
-        }
+            mPlateColor.startColor = oldPlateColor;
+            mPlateColor.progress.value = 0;
+            mPlateColor.onUpdate();
 
-        if (animate) {
-            ValueAnimator plateColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(),
-                    oldPlateColor, newPlateColor);
-            plateColorAnim.addUpdateListener(valueAnimator -> {
-                mPlateColor = (int) valueAnimator.getAnimatedValue();
-                invalidate();
-            });
             AnimatorSet changeIconAnim = new AnimatorSet();
-            if (slotMachineAnim != null) {
+
+            ObjectAnimator plateColorAnim =
+                    ObjectAnimator.ofFloat(mPlateColor.progress, AnimatedFloat.VALUE, 0, 1);
+            plateColorAnim.setAutoCancel(true);
+            changeIconAnim.play(plateColorAnim);
+
+            if (!mIsPinned && oldIcon != null) {
+                // Play the slot machine icon
+                mSlotMachineIcon = oldIcon;
+
+                float finalTrans = -getSlotMachineIconPlusSpacingSize();
+                Keyframe[] keyframes = new Keyframe[] {
+                        Keyframe.ofFloat(0f, 0f),
+                        Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
+                        Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
+                };
+                keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
+                keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
+
+                ObjectAnimator slotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
+                        PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
+                slotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
+                    mSlotMachineIcon = null;
+                    mSlotMachineIconTranslationY = 0;
+                    invalidate();
+                }));
+                slotMachineAnim.setAutoCancel(true);
                 changeIconAnim.play(slotMachineAnim);
             }
-            changeIconAnim.play(plateColorAnim);
+
             changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER);
             changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start();
         }
-    }
-
-    /**
-     * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
-     * and ending with the original icon.
-     */
-    public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate) {
-        return createSlotMachineAnim(iconsToAnimate, true);
-    }
-
-    /**
-     * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
-     * with the original icon, then cycling through the given icons, optionally ending back with
-     * the original icon.
-     * @param endWithOriginalIcon Whether we should land back on the icon we started with, rather
-     *                            than the last item in iconsToAnimate.
-     */
-    public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate,
-            boolean endWithOriginalIcon) {
-        if (mIsPinned || iconsToAnimate == null || iconsToAnimate.isEmpty()) {
-            return null;
-        }
-        if (mSlotMachineAnim != null) {
-            mSlotMachineAnim.end();
-        }
-
-        // Bookend the other animating icons with the original icon on both ends.
-        mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2);
-        mSlotMachineIcons.add(getIcon());
-        iconsToAnimate.stream()
-                .map(iconInfo -> iconInfo.newIcon(mContext, FLAG_THEMED))
-                .forEach(mSlotMachineIcons::add);
-        if (endWithOriginalIcon) {
-            mSlotMachineIcons.add(getIcon());
-        }
-
-        float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1);
-        Keyframe[] keyframes = new Keyframe[] {
-                Keyframe.ofFloat(0f, 0f),
-                Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
-                Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
-        };
-        keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
-        keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
-
-        mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
-                PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
-        mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
-            mSlotMachineIcons = null;
-            mSlotMachineAnim = null;
-            mSlotMachineIconTranslationY = 0;
-            invalidate();
-        }));
-        return mSlotMachineAnim;
+        return animate;
     }
 
     /**
@@ -345,6 +313,7 @@
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
+        mSlotIconBound.offsetTo((w - getIconSize()) / 2, (h - getIconSize()) / 2);
         updateRingPath();
     }
 
@@ -355,18 +324,12 @@
     }
 
     private void updateRingPath() {
-        boolean isBadged = false;
-        if (getTag() instanceof WorkspaceItemInfo) {
-            WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
-            isBadged = !Process.myUserHandle().equals(info.user)
-                    || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-        }
-
         mRingPath.reset();
         mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
-
         mRingPath.addPath(mShapePath, mTmpMatrix);
-        if (isBadged) {
+
+        FastBitmapDrawable icon = getIcon();
+        if (icon != null && icon.getBadge() != null) {
             float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
             float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
             float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
@@ -422,7 +385,7 @@
             canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
         }
         canvas.drawPath(mRingPath, mIconRingPaint);
-        mIconRingPaint.setColor(mPlateColor);
+        mIconRingPaint.setColor(mPlateColor.currentColor);
         mIconRingPaint.setMaskFilter(null);
         canvas.drawPath(mRingPath, mIconRingPaint);
         canvas.restoreToCount(count);
@@ -474,6 +437,21 @@
         return icon;
     }
 
+    private class AnimColorHolder {
+
+        public final AnimatedFloat progress = new AnimatedFloat(this::onUpdate, 1);
+        public final ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
+        public Integer startColor = 0;
+        public Integer endColor = 0;
+
+        public int currentColor = 0;
+
+        private void onUpdate() {
+            currentColor = (Integer) evaluator.evaluate(progress.value, startColor, endColor);
+            invalidate();
+        }
+    }
+
     /**
      * Draws Predicted Icon outline on cell layout
      */
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
deleted file mode 100644
index 111069f..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2017 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.uioverrides;
-
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
-import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
-import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
-import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
-import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
-import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION;
-import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION;
-import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
-import static com.android.wm.shell.Flags.enableSplitContextual;
-
-import android.animation.AnimatorSet;
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.util.FloatProperty;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.AnimUtils;
-import com.android.quickstep.util.SplitAnimationTimings;
-import com.android.quickstep.views.ClearAllButton;
-import com.android.quickstep.views.LauncherRecentsView;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
- * the basic view properties, this class also manages changes in the task visuals.
- */
-@TargetApi(Build.VERSION_CODES.O)
-public final class RecentsViewStateController extends
-        BaseRecentsViewStateController<LauncherRecentsView> {
-
-    public RecentsViewStateController(QuickstepLauncher launcher) {
-        super(launcher);
-    }
-
-    @Override
-    public void setState(@NonNull LauncherState state) {
-        super.setState(state);
-        if (state.isRecentsViewVisible) {
-            mRecentsView.updateEmptyMessage();
-        } else {
-            mRecentsView.resetTaskVisuals();
-        }
-        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
-        mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
-        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
-        // DepthController to prevent optimizations which might occlude the layers behind
-        mLauncher.getDepthController().setHasContentBehindLauncher(state.isRecentsViewVisible);
-
-        PendingAnimation builder =
-                new PendingAnimation(state.getTransitionDuration(mLauncher, true));
-
-        handleSplitSelectionState(state, builder, /* animate */false);
-    }
-
-    @Override
-    void setStateWithAnimationInternal(@NonNull LauncherState toState,
-            @NonNull StateAnimationConfig config, @NonNull PendingAnimation builder) {
-        super.setStateWithAnimationInternal(toState, config, builder);
-
-        if (toState.isRecentsViewVisible) {
-            // While animating into recents, update the visible task data as needed
-            builder.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
-            mRecentsView.updateEmptyMessage();
-            // TODO(b/246283207): Remove logging once root cause of flake detected.
-            if (Utilities.isRunningInTestHarness()) {
-                Log.d("b/246283207", "RecentsView#setStateWithAnimationInternal getCurrentPage(): "
-                                + mRecentsView.getCurrentPage()
-                                + ", getScrollForPage(getCurrentPage())): "
-                                + mRecentsView.getScrollForPage(mRecentsView.getCurrentPage()));
-            }
-        } else {
-            builder.addListener(
-                    AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals));
-        }
-        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
-        // DepthController to prevent optimizations which might occlude the layers behind
-        builder.addListener(AnimatorListeners.forSuccessCallback(() ->
-                mLauncher.getDepthController().setHasContentBehindLauncher(
-                        toState.isRecentsViewVisible)));
-
-        handleSplitSelectionState(toState, builder, /* animate */true);
-
-        setAlphas(builder, config, toState);
-        builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
-                toState.getOverviewFullscreenProgress(), LINEAR);
-    }
-
-    /**
-     * Create or dismiss split screen select animations.
-     * @param builder if null then this will run the split select animations right away, otherwise
-     *                will add animations to builder.
-     */
-    private void handleSplitSelectionState(@NonNull LauncherState toState,
-            @NonNull PendingAnimation builder, boolean animate) {
-        boolean goingToOverviewFromWorkspaceContextual = enableSplitContextual() &&
-                toState == OVERVIEW && mLauncher.isSplitSelectionActive();
-        if (toState != OVERVIEW_SPLIT_SELECT && !goingToOverviewFromWorkspaceContextual) {
-            // Not going to split
-            return;
-        }
-
-        // Create transition animations to split select
-        RecentsPagedOrientationHandler orientationHandler =
-                ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
-        Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
-                orientationHandler.getSplitSelectTaskOffset(
-                        TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
-                        mLauncher.getDeviceProfile());
-
-        SplitAnimationTimings timings =
-                AnimUtils.getDeviceOverviewToSplitTimings(mLauncher.getDeviceProfile().isTablet);
-        if (!goingToOverviewFromWorkspaceContextual) {
-            // This animation is already done for the contextual case, don't redo it
-            mRecentsView.createSplitSelectInitAnimation(builder,
-                    toState.getTransitionDuration(mLauncher, true /* isToState */));
-        }
-        // Shift tasks vertically downward to get out of placeholder view
-        builder.setFloat(mRecentsView, taskViewsFloat.first,
-                toState.getSplitSelectTranslation(mLauncher),
-                timings.getGridSlidePrimaryInterpolator());
-        // Zero out horizontal translation
-        builder.setFloat(mRecentsView, taskViewsFloat.second,
-                0,
-                timings.getGridSlideSecondaryInterpolator());
-
-        mRecentsView.handleDesktopTaskInSplitSelectState(builder,
-                timings.getDesktopTaskFadeInterpolator());
-
-        if (!animate) {
-            AnimatorSet as = builder.buildAnim();
-            as.start();
-            as.end();
-        }
-    }
-
-    private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
-            LauncherState state) {
-        float clearAllButtonAlpha = state.areElementsVisible(mLauncher, CLEAR_ALL_BUTTON) ? 1 : 0;
-        propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
-                clearAllButtonAlpha, LINEAR);
-        float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS) ? 1 : 0;
-        propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
-                AnimatedFloat.VALUE, overviewButtonAlpha, config.getInterpolator(
-                        ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
-    }
-
-    @Override
-    FloatProperty<RecentsView> getTaskModalnessProperty() {
-        return TASK_MODALNESS;
-    }
-
-    @Override
-    FloatProperty<RecentsView> getContentAlphaProperty() {
-        return CONTENT_ALPHA;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
new file mode 100644
index 0000000..f196548
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2025 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.uioverrides
+
+import com.android.app.animation.Interpolators.ACCELERATE_DECELERATE
+import com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT
+import com.android.app.animation.Interpolators.FINAL_FRAME
+import com.android.app.animation.Interpolators.INSTANT
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
+import com.android.launcher3.LauncherState
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.anim.AnimatorListeners.forSuccessCallback
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.anim.PropertySetter
+import com.android.launcher3.config.FeatureFlags.enableSplitContextually
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent
+import com.android.launcher3.statemanager.StateManager.StateHandler
+import com.android.launcher3.states.StateAnimationConfig
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X
+import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y
+import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW
+import com.android.quickstep.util.AnimUtils
+import com.android.quickstep.views.ClearAllButton
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET
+import com.android.quickstep.views.RecentsView.CONTENT_ALPHA
+import com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS
+import com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS
+import com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
+import com.android.quickstep.views.RecentsView.TASK_MODALNESS
+import com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION
+import com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION
+import com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION
+import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA
+import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL
+import com.android.wm.shell.Flags.enableSplitContextual
+
+/**
+ * State handler for handling UI changes for [com.android.quickstep.views.LauncherRecentsView]. In
+ * addition to managing the basic view properties, this class also manages changes in the task
+ * visuals.
+ */
+class RecentsViewStateController(private val launcher: QuickstepLauncher) :
+    StateHandler<LauncherState> {
+    private val recentsView: RecentsView<*, *> = launcher.getOverviewPanel()
+
+    override fun setState(state: LauncherState) {
+        val scaleAndOffset = state.getOverviewScaleAndOffset(launcher)
+        RECENTS_SCALE_PROPERTY.set(recentsView, scaleAndOffset[0])
+        ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, scaleAndOffset[1])
+        TASK_SECONDARY_TRANSLATION.set(recentsView, 0f)
+
+        CONTENT_ALPHA.set(recentsView, if (state.isRecentsViewVisible) 1f else 0f)
+        TASK_MODALNESS.set(recentsView, state.overviewModalness)
+        RECENTS_GRID_PROGRESS.set(
+            recentsView,
+            if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
+        )
+        TASK_THUMBNAIL_SPLASH_ALPHA.set(
+            recentsView,
+            if (state.showTaskThumbnailSplash()) 1f else 0f,
+        )
+        if (enableLargeDesktopWindowingTile()) {
+            DESKTOP_CAROUSEL_DETACH_PROGRESS.set(
+                recentsView,
+                if (state.detachDesktopCarousel()) 1f else 0f,
+            )
+        }
+
+        if (state.isRecentsViewVisible) {
+            recentsView.updateEmptyMessage()
+        } else {
+            recentsView.resetTaskVisuals()
+        }
+        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, StateAnimationConfig(), state)
+        recentsView.setFullscreenProgress(state.overviewFullscreenProgress)
+        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+        // DepthController to prevent optimizations which might occlude the layers behind
+        launcher.depthController.setHasContentBehindLauncher(state.isRecentsViewVisible)
+
+        val builder = PendingAnimation(state.getTransitionDuration(launcher, true).toLong())
+        handleSplitSelectionState(state, builder, animate = false)
+    }
+
+    override fun setStateWithAnimation(
+        toState: LauncherState,
+        config: StateAnimationConfig,
+        builder: PendingAnimation,
+    ) {
+        if (config.hasAnimationFlag(SKIP_OVERVIEW)) return
+
+        val scaleAndOffset = toState.getOverviewScaleAndOffset(launcher)
+        builder.setFloat(
+            recentsView,
+            RECENTS_SCALE_PROPERTY,
+            scaleAndOffset[0],
+            config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR),
+        )
+        builder.setFloat(
+            recentsView,
+            ADJACENT_PAGE_HORIZONTAL_OFFSET,
+            scaleAndOffset[1],
+            config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR),
+        )
+        builder.setFloat(
+            recentsView,
+            TASK_SECONDARY_TRANSLATION,
+            0f,
+            config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR),
+        )
+
+        val exitingOverview = !enableSplitContextually() && !toState.isRecentsViewVisible
+        if (recentsView.isSplitSelectionActive && exitingOverview) {
+            builder.add(
+                recentsView.splitSelectController.splitAnimationController
+                    .createPlaceholderDismissAnim(
+                        launcher,
+                        LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+                        builder.duration,
+                    )
+            )
+            builder.setViewAlpha(
+                recentsView.splitInstructionsView,
+                0f,
+                config.getInterpolator(ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE, LINEAR),
+            )
+        }
+
+        builder.setFloat(
+            recentsView,
+            CONTENT_ALPHA,
+            if (toState.isRecentsViewVisible) 1f else 0f,
+            config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT),
+        )
+
+        builder.setFloat(
+            recentsView,
+            TASK_MODALNESS,
+            toState.overviewModalness,
+            config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR),
+        )
+
+        val fromState = launcher.stateManager.state
+        builder.setFloat(
+            recentsView,
+            TASK_THUMBNAIL_SPLASH_ALPHA,
+            if (toState.showTaskThumbnailSplash()) 1f else 0f,
+            getOverviewInterpolator(fromState, toState),
+        )
+
+        builder.setFloat(
+            recentsView,
+            RECENTS_GRID_PROGRESS,
+            if (toState.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
+            getOverviewInterpolator(fromState, toState),
+        )
+
+        if (enableLargeDesktopWindowingTile()) {
+            builder.setFloat(
+                recentsView,
+                DESKTOP_CAROUSEL_DETACH_PROGRESS,
+                if (toState.detachDesktopCarousel()) 1f else 0f,
+                getOverviewInterpolator(fromState, toState),
+            )
+        }
+
+        if (toState.isRecentsViewVisible) {
+            // While animating into recents, update the visible task data as needed
+            builder.addOnFrameCallback { recentsView.loadVisibleTaskData(FLAG_UPDATE_ALL) }
+            recentsView.updateEmptyMessage()
+        } else {
+            builder.addListener(forSuccessCallback { recentsView.resetTaskVisuals() })
+        }
+        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+        // DepthController to prevent optimizations which might occlude the layers behind
+        builder.addListener(
+            forSuccessCallback {
+                launcher.depthController.setHasContentBehindLauncher(toState.isRecentsViewVisible)
+            }
+        )
+
+        handleSplitSelectionState(toState, builder, animate = true)
+
+        setAlphas(builder, config, toState)
+        builder.setFloat(
+            recentsView,
+            FULLSCREEN_PROGRESS,
+            toState.overviewFullscreenProgress,
+            LINEAR,
+        )
+
+        builder.addEndListener { success: Boolean ->
+            if (!success && !toState.isRecentsViewVisible) {
+                recentsView.reset()
+            }
+        }
+    }
+
+    /**
+     * Create or dismiss split screen select animations.
+     *
+     * @param builder if null then this will run the split select animations right away, otherwise
+     *   will add animations to builder.
+     */
+    private fun handleSplitSelectionState(
+        toState: LauncherState,
+        builder: PendingAnimation,
+        animate: Boolean,
+    ) {
+        val goingToOverviewFromWorkspaceContextual =
+            enableSplitContextual() &&
+                toState == LauncherState.OVERVIEW &&
+                launcher.isSplitSelectionActive
+        if (
+            toState != LauncherState.OVERVIEW_SPLIT_SELECT &&
+                !goingToOverviewFromWorkspaceContextual
+        ) {
+            // Not going to split
+            return
+        }
+
+        // Create transition animations to split select
+        val orientationHandler = recentsView.pagedOrientationHandler
+        val taskViewsFloat =
+            orientationHandler.getSplitSelectTaskOffset(
+                TASK_PRIMARY_SPLIT_TRANSLATION,
+                TASK_SECONDARY_SPLIT_TRANSLATION,
+                launcher.deviceProfile,
+            )
+
+        val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
+        if (!goingToOverviewFromWorkspaceContextual) {
+            // This animation is already done for the contextual case, don't redo it
+            recentsView.createSplitSelectInitAnimation(
+                builder,
+                toState.getTransitionDuration(launcher, true),
+            )
+        }
+        // Shift tasks vertically downward to get out of placeholder view
+        builder.setFloat(
+            recentsView,
+            taskViewsFloat.first,
+            toState.getSplitSelectTranslation(launcher),
+            timings.gridSlidePrimaryInterpolator,
+        )
+        // Zero out horizontal translation
+        builder.setFloat(
+            recentsView,
+            taskViewsFloat.second,
+            0f,
+            timings.gridSlideSecondaryInterpolator,
+        )
+
+        recentsView.handleDesktopTaskInSplitSelectState(
+            builder,
+            timings.desktopTaskFadeInterpolator,
+        )
+
+        if (!animate) {
+            builder.buildAnim().apply {
+                start()
+                end()
+            }
+        }
+    }
+
+    private fun setAlphas(
+        propertySetter: PropertySetter,
+        config: StateAnimationConfig,
+        state: LauncherState,
+    ) {
+        val clearAllButtonAlpha =
+            if (state.areElementsVisible(launcher, LauncherState.CLEAR_ALL_BUTTON)) 1f else 0f
+        propertySetter.setFloat(
+            recentsView.clearAllButton,
+            ClearAllButton.VISIBILITY_ALPHA,
+            clearAllButtonAlpha,
+            LINEAR,
+        )
+        val overviewButtonAlpha =
+            if (state.areElementsVisible(launcher, LauncherState.OVERVIEW_ACTIONS)) 1f else 0f
+        propertySetter.setFloat(
+            launcher.actionsView.visibilityAlpha,
+            AnimatedFloat.VALUE,
+            overviewButtonAlpha,
+            config.getInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, LINEAR),
+        )
+    }
+
+    private fun getOverviewInterpolator(fromState: LauncherState, toState: LauncherState) =
+        when {
+            fromState == LauncherState.QUICK_SWITCH_FROM_HOME -> ACCELERATE_DECELERATE
+            toState.isRecentsViewVisible -> INSTANT
+            else -> FINAL_FRAME
+        }
+}
diff --git a/quickstep/src/com/android/quickstep/HighResLoadingState.kt b/quickstep/src/com/android/quickstep/HighResLoadingState.kt
new file mode 100644
index 0000000..8a21c4f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/HighResLoadingState.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.content.res.Resources
+import com.android.quickstep.recents.data.HighResLoadingStateNotifier
+
+/** Determines when high res or low res thumbnails should be loaded. */
+class HighResLoadingState : HighResLoadingStateNotifier {
+    // If the device does not support low-res thumbnails, only attempt to load high-res thumbnails
+    private val forceHighResThumbnails = !supportsLowResThumbnails()
+    var visible: Boolean = false
+        set(value) {
+            field = value
+            updateState()
+        }
+
+    var flingingFast = false
+        set(value) {
+            field = value
+            updateState()
+        }
+
+    var isEnabled: Boolean = false
+        private set
+
+    private val callbacks = ArrayList<HighResLoadingStateChangedCallback>()
+
+    interface HighResLoadingStateChangedCallback {
+        fun onHighResLoadingStateChanged(enabled: Boolean)
+    }
+
+    override fun addCallback(callback: HighResLoadingStateChangedCallback) {
+        callbacks.add(callback)
+    }
+
+    override fun removeCallback(callback: HighResLoadingStateChangedCallback) {
+        callbacks.remove(callback)
+    }
+
+    private fun updateState() {
+        val prevState = isEnabled
+        isEnabled = forceHighResThumbnails || (visible && !flingingFast)
+        if (prevState != isEnabled) {
+            for (callback in callbacks.asReversed()) {
+                callback.onHighResLoadingStateChanged(isEnabled)
+            }
+        }
+    }
+
+    /**
+     * Returns Whether device supports low-res thumbnails. Low-res files are an optimization for
+     * faster load times of snapshots. Devices can optionally disable low-res files so that they
+     * only store snapshots at high-res scale. The actual scale can be configured in frameworks/base
+     * config overlay.
+     */
+    private fun supportsLowResThumbnails(): Boolean {
+        val res = Resources.getSystem()
+        val resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android")
+        if (resId != 0) {
+            return 0 < res.getFloat(resId)
+        }
+        return true
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
deleted file mode 100644
index c4221a1..0000000
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ /dev/null
@@ -1,308 +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.Flags.enableOverviewIconMenu;
-import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.ActivityManager.TaskDescription;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.SparseArray;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.icons.BaseIconFactory;
-import com.android.launcher3.icons.BaseIconFactory.IconOptions;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconProvider;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.CancellableTask;
-import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
-import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.FlagOp;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.task.thumbnail.data.TaskIconDataSource;
-import com.android.quickstep.util.TaskKeyLruCache;
-import com.android.quickstep.util.TaskVisualsChangeListener;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.system.PackageManagerWrapper;
-
-import java.util.concurrent.Executor;
-
-/**
- * Manages the caching of task icons and related data.
- */
-public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListener {
-
-    private final Executor mBgExecutor;
-
-    private final Context mContext;
-    private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
-    private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
-    private BitmapInfo mDefaultIconBase = null;
-
-    private final IconProvider mIconProvider;
-
-    private BaseIconFactory mIconFactory;
-
-    @Nullable
-    public TaskVisualsChangeListener mTaskVisualsChangeListener = null;
-
-    public TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider) {
-        mContext = context;
-        mBgExecutor = bgExecutor;
-        mIconProvider = iconProvider;
-
-        Resources res = context.getResources();
-        int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
-
-        mIconCache = new TaskKeyLruCache<>(cacheSize);
-
-        DisplayController.INSTANCE.get(mContext).addChangeListener(this);
-    }
-
-    @Override
-    public void onDisplayInfoChanged(Context context, Info info, int flags) {
-        if ((flags & CHANGE_DENSITY) != 0) {
-            clearCache();
-        }
-    }
-
-    /**
-     * Asynchronously fetches the icon and other task data.
-     *
-     * @param task The task to fetch the data for
-     * @param callback The callback to receive the task after its data has been populated.
-     * @return A cancelable handle to the request
-     */
-    @Override
-    public CancellableTask getIconInBackground(Task task, @NonNull GetTaskIconCallback callback) {
-        Preconditions.assertUIThread();
-        if (task.icon != null) {
-            // Nothing to load, the icon is already loaded
-            callback.onTaskIconReceived(task.icon, task.titleDescription, task.title);
-            return null;
-        }
-        CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
-                () -> getCacheEntry(task),
-                MAIN_EXECUTOR,
-                result -> {
-                    task.icon = result.icon;
-                    task.titleDescription = result.contentDescription;
-                    task.title = result.title;
-
-                    callback.onTaskIconReceived(
-                            result.icon,
-                            result.contentDescription,
-                            result.title);
-                    dispatchIconUpdate(task.key.id);
-                }
-        );
-        mBgExecutor.execute(request);
-        return request;
-    }
-
-    /**
-     * Clears the icon cache
-     */
-    public void clearCache() {
-        mBgExecutor.execute(this::resetFactory);
-    }
-
-    void onTaskRemoved(TaskKey taskKey) {
-        mIconCache.remove(taskKey);
-    }
-
-    void invalidateCacheEntries(String pkg, UserHandle handle) {
-        mBgExecutor.execute(() -> mIconCache.removeAll(key ->
-                pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
-    }
-
-    @WorkerThread
-    private TaskCacheEntry getCacheEntry(Task task) {
-        TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key);
-        if (entry != null) {
-            return entry;
-        }
-
-        TaskDescription desc = task.taskDescription;
-        TaskKey key = task.key;
-        ActivityInfo activityInfo = null;
-
-        // Create new cache entry
-        entry = new TaskCacheEntry();
-
-        // Load icon
-        // TODO: Load icon resource (b/143363444)
-        Bitmap icon = getIcon(desc, key.userId);
-        if (icon != null) {
-            entry.icon = getBitmapInfo(
-                    new BitmapDrawable(mContext.getResources(), icon),
-                    key.userId,
-                    desc.getPrimaryColor(),
-                    false /* isInstantApp */).newIcon(mContext);
-        } else {
-            activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
-                    key.getComponent(), key.userId);
-            if (activityInfo != null) {
-                BitmapInfo bitmapInfo = getBitmapInfo(
-                        mIconProvider.getIcon(activityInfo),
-                        key.userId,
-                        desc.getPrimaryColor(),
-                        activityInfo.applicationInfo.isInstantApp());
-                entry.icon = bitmapInfo.newIcon(mContext);
-            } else {
-                entry.icon = getDefaultIcon(key.userId);
-            }
-        }
-
-        // Skip loading the content description if the activity no longer exists
-        if (activityInfo == null) {
-            activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
-                    key.getComponent(), key.userId);
-        }
-        if (activityInfo != null) {
-            entry.contentDescription = getBadgedContentDescription(
-                    activityInfo, task.key.userId, task.taskDescription);
-            if (enableOverviewIconMenu()) {
-                entry.title = Utilities.trim(activityInfo.loadLabel(mContext.getPackageManager()));
-            }
-        }
-
-        mIconCache.put(task.key, entry);
-        return entry;
-    }
-
-    private Bitmap getIcon(ActivityManager.TaskDescription desc, int userId) {
-        if (desc.getInMemoryIcon() != null) {
-            return desc.getInMemoryIcon();
-        }
-        return ActivityManager.TaskDescription.loadTaskDescriptionIcon(
-                desc.getIconFilename(), userId);
-    }
-
-    private String getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td) {
-        PackageManager pm = mContext.getPackageManager();
-        String taskLabel = td == null ? null : Utilities.trim(td.getLabel());
-        if (TextUtils.isEmpty(taskLabel)) {
-            taskLabel = Utilities.trim(info.loadLabel(pm));
-        }
-
-        String applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(pm));
-        String badgedApplicationLabel = userId != UserHandle.myUserId()
-                ? pm.getUserBadgedLabel(applicationLabel, UserHandle.of(userId)).toString()
-                : applicationLabel;
-        return applicationLabel.equals(taskLabel)
-                ? badgedApplicationLabel : badgedApplicationLabel + " " + taskLabel;
-    }
-
-    @WorkerThread
-    private Drawable getDefaultIcon(int userId) {
-        synchronized (mDefaultIcons) {
-            if (mDefaultIconBase == null) {
-                try (BaseIconFactory bif = getIconFactory()) {
-                    mDefaultIconBase = bif.makeDefaultIcon(mIconProvider);
-                }
-            }
-
-            int index;
-            if ((index = mDefaultIcons.indexOfKey(userId)) >= 0) {
-                return mDefaultIcons.valueAt(index).newIcon(mContext);
-            } else {
-                BitmapInfo info = mDefaultIconBase.withFlags(
-                        UserCache.INSTANCE.get(mContext).getUserInfo(UserHandle.of(userId))
-                                .applyBitmapInfoFlags(FlagOp.NO_OP));
-                mDefaultIcons.put(userId, info);
-                return info.newIcon(mContext);
-            }
-        }
-    }
-
-    @WorkerThread
-    private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
-            int primaryColor, boolean isInstantApp) {
-        try (BaseIconFactory bif = getIconFactory()) {
-            bif.setWrapperBackgroundColor(primaryColor);
-
-            // User version code O, so that the icon is always wrapped in an adaptive icon container
-            return bif.createBadgedIconBitmap(drawable,
-                    new IconOptions()
-                            .setUser(UserCache.INSTANCE.get(mContext)
-                                    .getUserInfo(UserHandle.of(userId)))
-                            .setInstantApp(isInstantApp)
-                            .setExtractedColor(0));
-        }
-    }
-
-    @WorkerThread
-    private BaseIconFactory getIconFactory() {
-        if (mIconFactory == null) {
-            mIconFactory = new BaseIconFactory(mContext,
-                    DisplayController.INSTANCE.get(mContext).getInfo().getDensityDpi(),
-                    mContext.getResources().getDimensionPixelSize(
-                            R.dimen.task_icon_cache_default_icon_size));
-        }
-        return mIconFactory;
-    }
-
-    @WorkerThread
-    private void resetFactory() {
-        mIconFactory = null;
-        mIconCache.evictAll();
-    }
-
-    private static class TaskCacheEntry {
-        public Drawable icon;
-        public String contentDescription = "";
-        public String title = "";
-    }
-
-    /** Callback used when retrieving app icons from cache. */
-    public interface GetTaskIconCallback {
-        /** Called when task icon is retrieved. */
-        void onTaskIconReceived(Drawable icon, String contentDescription, String title);
-    }
-
-    void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) {
-        mTaskVisualsChangeListener = newListener;
-    }
-
-    void removeTaskVisualsChangeListener() {
-        mTaskVisualsChangeListener = null;
-    }
-
-    void dispatchIconUpdate(int taskId) {
-        if (mTaskVisualsChangeListener != null) {
-            mTaskVisualsChangeListener.onTaskIconChanged(taskId);
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
new file mode 100644
index 0000000..bf94d41
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2025 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.app.ActivityManager
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.util.SparseArray
+import androidx.annotation.WorkerThread
+import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.BaseIconFactory.IconOptions
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.icons.IconProvider
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.FlagOp
+import com.android.launcher3.util.Preconditions
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
+import com.android.quickstep.util.TaskKeyLruCache
+import com.android.quickstep.util.TaskVisualsChangeListener
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.system.PackageManagerWrapper
+import java.util.concurrent.Executor
+
+/** Manages the caching of task icons and related data. */
+class TaskIconCache(
+    private val context: Context,
+    private val bgExecutor: Executor,
+    private val iconProvider: IconProvider,
+) : TaskIconDataSource, DisplayInfoChangeListener {
+    private val iconCache =
+        TaskKeyLruCache<TaskCacheEntry>(
+            context.resources.getInteger(R.integer.recentsIconCacheSize)
+        )
+    private val defaultIcons = SparseArray<BitmapInfo>()
+    private var defaultIconBase: BitmapInfo? = null
+
+    private var _iconFactory: BaseIconFactory? = null
+    @get:WorkerThread
+    private val iconFactory: BaseIconFactory
+        get() =
+            if (enableRefactorTaskThumbnail()) createIconFactory()
+            else _iconFactory ?: createIconFactory().also { _iconFactory = it }
+
+    var taskVisualsChangeListener: TaskVisualsChangeListener? = null
+
+    init {
+        DisplayController.INSTANCE.get(context).addChangeListener(this)
+    }
+
+    override fun onDisplayInfoChanged(context: Context, info: DisplayController.Info, flags: Int) {
+        if ((flags and DisplayController.CHANGE_DENSITY) != 0) {
+            clearCache()
+        }
+    }
+
+    // TODO(b/387496731): Add ensureActive() calls if they show performance benefit
+    override suspend fun getIcon(task: Task): TaskCacheEntry {
+        task.icon?.let {
+            // Nothing to load, the icon is already loaded
+            return TaskCacheEntry(it, task.titleDescription ?: "", task.title)
+        }
+
+        val entry = getCacheEntry(task)
+        task.icon = entry.icon
+        task.titleDescription = entry.contentDescription
+        task.title = entry.title
+
+        dispatchIconUpdate(task.key.id)
+        return entry
+    }
+
+    /**
+     * Asynchronously fetches the icon and other task data.
+     *
+     * @param task The task to fetch the data for
+     * @param callback The callback to receive the task after its data has been populated.
+     * @return A cancelable handle to the request
+     */
+    fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>? {
+        Preconditions.assertUIThread()
+        task.icon?.let {
+            // Nothing to load, the icon is already loaded
+            callback.onTaskIconReceived(it, task.titleDescription ?: "", task.title ?: "")
+            return null
+        }
+        val request =
+            CancellableTask(
+                { getCacheEntry(task) },
+                Executors.MAIN_EXECUTOR,
+                { result: TaskCacheEntry ->
+                    task.icon = result.icon
+                    task.titleDescription = result.contentDescription
+                    task.title = result.title
+
+                    callback.onTaskIconReceived(
+                        result.icon,
+                        result.contentDescription,
+                        result.title,
+                    )
+                    dispatchIconUpdate(task.key.id)
+                },
+            )
+        bgExecutor.execute(request)
+        return request
+    }
+
+    /** Clears the icon cache */
+    fun clearCache() {
+        bgExecutor.execute { resetFactory() }
+    }
+
+    fun onTaskRemoved(taskKey: TaskKey) {
+        iconCache.remove(taskKey)
+    }
+
+    fun invalidateCacheEntries(pkg: String, handle: UserHandle) {
+        bgExecutor.execute {
+            iconCache.removeAll { key: TaskKey ->
+                pkg == key.packageName && handle.identifier == key.userId
+            }
+        }
+    }
+
+    @WorkerThread
+    private fun createIconFactory() =
+        BaseIconFactory(
+            context,
+            DisplayController.INSTANCE.get(context).info.densityDpi,
+            context.resources.getDimensionPixelSize(R.dimen.task_icon_cache_default_icon_size),
+        )
+
+    @WorkerThread
+    private fun getCacheEntry(task: Task): TaskCacheEntry {
+        iconCache.getAndInvalidateIfModified(task.key)?.let {
+            return it
+        }
+
+        val desc = task.taskDescription
+        val key = task.key
+        var activityInfo: ActivityInfo? = null
+
+        // Create new cache entry
+
+        // Load icon
+        val icon = getIcon(desc, key.userId)
+        val entryIcon =
+            if (icon != null) {
+                getBitmapInfo(
+                        BitmapDrawable(context.resources, icon),
+                        key.userId,
+                        desc.primaryColor,
+                        false, /* isInstantApp */
+                    )
+                    .newIcon(context)
+            } else {
+                activityInfo =
+                    PackageManagerWrapper.getInstance().getActivityInfo(key.component, key.userId)
+                if (activityInfo != null) {
+                    val bitmapInfo =
+                        getBitmapInfo(
+                            iconProvider.getIcon(activityInfo),
+                            key.userId,
+                            desc.primaryColor,
+                            activityInfo.applicationInfo.isInstantApp,
+                        )
+                    bitmapInfo.newIcon(context)
+                } else {
+                    getDefaultIcon(key.userId)
+                }
+            }
+
+        activityInfo =
+            activityInfo
+                ?: PackageManagerWrapper.getInstance().getActivityInfo(key.component, key.userId)
+
+        return when {
+            // Skip loading the content description if the activity no longer exists
+            activityInfo == null -> TaskCacheEntry(entryIcon)
+            enableOverviewIconMenu() ->
+                TaskCacheEntry(
+                    entryIcon,
+                    getBadgedContentDescription(
+                        activityInfo,
+                        task.key.userId,
+                        task.taskDescription,
+                    ),
+                    Utilities.trim(activityInfo.loadLabel(context.packageManager)),
+                )
+            else ->
+                TaskCacheEntry(
+                    entryIcon,
+                    getBadgedContentDescription(activityInfo, task.key.userId, task.taskDescription),
+                )
+        }.also { iconCache.put(task.key, it) }
+    }
+
+    private fun getIcon(desc: ActivityManager.TaskDescription, userId: Int): Bitmap? =
+        desc.inMemoryIcon
+            ?: ActivityManager.TaskDescription.loadTaskDescriptionIcon(desc.iconFilename, userId)
+
+    private fun getBadgedContentDescription(
+        info: ActivityInfo,
+        userId: Int,
+        taskDescription: ActivityManager.TaskDescription?,
+    ): String {
+        val packageManager = context.packageManager
+        var taskLabel = taskDescription?.let { Utilities.trim(it.label) }
+        if (taskLabel.isNullOrEmpty()) {
+            taskLabel = Utilities.trim(info.loadLabel(packageManager))
+        }
+
+        val applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(packageManager))
+        val badgedApplicationLabel =
+            if (userId != UserHandle.myUserId())
+                packageManager
+                    .getUserBadgedLabel(applicationLabel, UserHandle.of(userId))
+                    .toString()
+            else applicationLabel
+        return if (applicationLabel == taskLabel) badgedApplicationLabel
+        else "$badgedApplicationLabel $taskLabel"
+    }
+
+    @WorkerThread
+    private fun getDefaultIcon(userId: Int): Drawable {
+        synchronized(defaultIcons) {
+            val defaultIconBase =
+                defaultIconBase ?: iconFactory.use { it.makeDefaultIcon(iconProvider) }
+            val index: Int = defaultIcons.indexOfKey(userId)
+            return if (index >= 0) {
+                defaultIcons.valueAt(index).newIcon(context)
+            } else {
+                val info =
+                    defaultIconBase.withFlags(
+                        UserCache.INSTANCE.get(context)
+                            .getUserInfo(UserHandle.of(userId))
+                            .applyBitmapInfoFlags(FlagOp.NO_OP)
+                    )
+                defaultIcons.put(userId, info)
+                info.newIcon(context)
+            }
+        }
+    }
+
+    @WorkerThread
+    private fun getBitmapInfo(
+        drawable: Drawable,
+        userId: Int,
+        primaryColor: Int,
+        isInstantApp: Boolean,
+    ): BitmapInfo {
+        iconFactory.use { iconFactory ->
+            iconFactory.setWrapperBackgroundColor(primaryColor)
+            // User version code O, so that the icon is always wrapped in an adaptive icon container
+            return iconFactory.createBadgedIconBitmap(
+                drawable,
+                IconOptions()
+                    .setUser(UserCache.INSTANCE.get(context).getUserInfo(UserHandle.of(userId)))
+                    .setInstantApp(isInstantApp)
+                    .setExtractedColor(0),
+            )
+        }
+    }
+
+    @WorkerThread
+    private fun resetFactory() {
+        _iconFactory = null
+        iconCache.evictAll()
+    }
+
+    data class TaskCacheEntry(
+        val icon: Drawable,
+        val contentDescription: String = "",
+        val title: String = "",
+    )
+
+    /** Callback used when retrieving app icons from cache. */
+    fun interface GetTaskIconCallback {
+        /** Called when task icon is retrieved. */
+        fun onTaskIconReceived(icon: Drawable, contentDescription: String, title: String)
+    }
+
+    fun registerTaskVisualsChangeListener(newListener: TaskVisualsChangeListener?) {
+        taskVisualsChangeListener = newListener
+    }
+
+    fun removeTaskVisualsChangeListener() {
+        taskVisualsChangeListener = null
+    }
+
+    private fun dispatchIconUpdate(taskId: Int) {
+        taskVisualsChangeListener?.onTaskIconChanged(taskId)
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
deleted file mode 100644
index 580dcc2..0000000
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ /dev/null
@@ -1,278 +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.Flags.enableGridOnlyOverview;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.CancellableTask;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.recents.data.HighResLoadingStateNotifier;
-import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
-import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
-import com.android.quickstep.util.TaskKeyCache;
-import com.android.quickstep.util.TaskKeyLruCache;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-
-public class TaskThumbnailCache implements TaskThumbnailDataSource {
-
-    private final Executor mBgExecutor;
-    private final TaskKeyCache<ThumbnailData> mCache;
-    private final HighResLoadingState mHighResLoadingState;
-    private final boolean mEnableTaskSnapshotPreloading;
-    private final Context mContext;
-
-    public static class HighResLoadingState implements HighResLoadingStateNotifier {
-        private boolean mForceHighResThumbnails;
-        private boolean mVisible;
-        private boolean mFlingingFast;
-        private boolean mHighResLoadingEnabled;
-        private ArrayList<HighResLoadingStateChangedCallback> mCallbacks = new ArrayList<>();
-
-        public interface HighResLoadingStateChangedCallback {
-            void onHighResLoadingStateChanged(boolean enabled);
-        }
-
-        private HighResLoadingState(Context context) {
-            // If the device does not support low-res thumbnails, only attempt to load high-res
-            // thumbnails
-            mForceHighResThumbnails = !supportsLowResThumbnails();
-        }
-
-        @Override
-        public void addCallback(@NonNull HighResLoadingStateChangedCallback callback) {
-            mCallbacks.add(callback);
-        }
-
-        @Override
-        public void removeCallback(@NonNull HighResLoadingStateChangedCallback callback) {
-            mCallbacks.remove(callback);
-        }
-
-        public void setVisible(boolean visible) {
-            mVisible = visible;
-            updateState();
-        }
-
-        public void setFlingingFast(boolean flingingFast) {
-            mFlingingFast = flingingFast;
-            updateState();
-        }
-
-        public boolean isEnabled() {
-            return mHighResLoadingEnabled;
-        }
-
-        private void updateState() {
-            boolean prevState = mHighResLoadingEnabled;
-            mHighResLoadingEnabled = mForceHighResThumbnails || (mVisible && !mFlingingFast);
-            if (prevState != mHighResLoadingEnabled) {
-                for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                    mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
-                }
-            }
-        }
-    }
-
-    public TaskThumbnailCache(Context context, Executor bgExecutor) {
-        this(context, bgExecutor,
-                context.getResources().getInteger(R.integer.recentsThumbnailCacheSize));
-    }
-
-    private TaskThumbnailCache(Context context, Executor bgExecutor, int cacheSize) {
-        this(context, bgExecutor,
-                enableGridOnlyOverview() ? new TaskKeyByLastActiveTimeCache<>(cacheSize)
-                        : new TaskKeyLruCache<>(cacheSize));
-    }
-
-    @VisibleForTesting
-    TaskThumbnailCache(Context context, Executor bgExecutor, TaskKeyCache<ThumbnailData> cache) {
-        mBgExecutor = bgExecutor;
-        mHighResLoadingState = new HighResLoadingState(context);
-        mContext = context;
-
-        Resources res = context.getResources();
-        mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
-        mCache = cache;
-    }
-
-    /**
-     * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
-     * puts it in the cache.
-     */
-    public void updateThumbnailInCache(Task task, boolean lowResolution) {
-        if (task == null) {
-            return;
-        }
-        Preconditions.assertUIThread();
-        // Fetch the thumbnail for this task and put it in the cache
-        if (task.thumbnail == null) {
-            getThumbnailInBackground(task.key, lowResolution, t -> task.thumbnail = t);
-        }
-    }
-
-    /**
-     * Synchronously updates the thumbnail in the cache if it is already there.
-     */
-    public void updateTaskSnapShot(int taskId, ThumbnailData thumbnail) {
-        Preconditions.assertUIThread();
-        mCache.updateIfAlreadyInCache(taskId, thumbnail);
-    }
-
-    /**
-     * Asynchronously fetches the thumbnail for the given {@code task}.
-     *
-     * @param callback The callback to receive the task after its data has been populated.
-     * @return A cancelable handle to the request
-     */
-    @Override
-    public CancellableTask<ThumbnailData> getThumbnailInBackground(
-            Task task, @NonNull Consumer<ThumbnailData> callback) {
-        Preconditions.assertUIThread();
-
-        boolean lowResolution = !mHighResLoadingState.isEnabled();
-        if (task.thumbnail != null && task.thumbnail.getThumbnail() != null
-                && (!task.thumbnail.reducedResolution || lowResolution)) {
-            // Nothing to load, the thumbnail is already high-resolution or matches what the
-            // request, so just callback
-            callback.accept(task.thumbnail);
-            return null;
-        }
-
-        return getThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), callback);
-    }
-
-    /**
-     * Updates cache size and remove excess entries if current size is more than new cache size.
-     *
-     * @return whether cache size has increased
-     */
-    public boolean updateCacheSizeAndRemoveExcess() {
-        int newSize = mContext.getResources().getInteger(R.integer.recentsThumbnailCacheSize);
-        int oldSize = mCache.getMaxSize();
-        if (newSize == oldSize) {
-            // Return if no change in size
-            return false;
-        }
-
-        mCache.updateCacheSizeAndRemoveExcess(newSize);
-        return newSize > oldSize;
-    }
-
-    private CancellableTask<ThumbnailData> getThumbnailInBackground(TaskKey key,
-            boolean lowResolution, Consumer<ThumbnailData> callback) {
-        Preconditions.assertUIThread();
-
-        ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
-        if (cachedThumbnail != null &&  cachedThumbnail.getThumbnail() != null
-                && (!cachedThumbnail.reducedResolution || lowResolution)) {
-            // Already cached, lets use that thumbnail
-            callback.accept(cachedThumbnail);
-            return null;
-        }
-
-        CancellableTask<ThumbnailData> request = new CancellableTask<>(
-                () -> {
-                    ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
-                            .getTaskThumbnail(key.id, lowResolution);
-                    return thumbnailData.getThumbnail() != null ? thumbnailData
-                            : ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
-                },
-                MAIN_EXECUTOR,
-                result -> {
-                    // Avoid an async timing issue that a low res entry replaces an existing high
-                    // res entry in high res enabled state, so we check before putting it to cache
-                    if (enableGridOnlyOverview() && result.reducedResolution
-                            && getHighResLoadingState().isEnabled()) {
-                        ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
-                        if (newCachedThumbnail != null && newCachedThumbnail.getThumbnail() != null
-                                && !newCachedThumbnail.reducedResolution) {
-                            return;
-                        }
-                    }
-                    mCache.put(key, result);
-                    callback.accept(result);
-                }
-        );
-        mBgExecutor.execute(request);
-        return request;
-    }
-
-    /**
-     * Clears the cache.
-     */
-    public void clear() {
-        mCache.evictAll();
-    }
-
-    /**
-     * Removes the cached thumbnail for the given task.
-     */
-    public void remove(Task.TaskKey key) {
-        mCache.remove(key);
-    }
-
-    /**
-     * @return The cache size.
-     */
-    public int getCacheSize() {
-        return mCache.getMaxSize();
-    }
-
-    /**
-     * @return The mutable high-res loading state.
-     */
-    public HighResLoadingState getHighResLoadingState() {
-        return mHighResLoadingState;
-    }
-
-    /**
-     * @return Whether to enable background preloading of task thumbnails.
-     */
-    public boolean isPreloadingEnabled() {
-        return mEnableTaskSnapshotPreloading && mHighResLoadingState.mVisible;
-    }
-
-    /**
-     * @return Whether device supports low-res thumbnails. Low-res files are an optimization
-     * for faster load times of snapshots. Devices can optionally disable low-res files so that
-     * they only store snapshots at high-res scale. The actual scale can be configured in
-     * frameworks/base config overlay.
-     */
-    private static boolean supportsLowResThumbnails() {
-        Resources res = Resources.getSystem();
-        int resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android");
-        if (resId != 0) {
-            return 0 < res.getFloat(resId);
-        }
-        return true;
-    }
-
-}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt b/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt
new file mode 100644
index 0000000..7b56213
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.launcher3.Flags.enableGridOnlyOverview
+import com.android.launcher3.R
+import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.Preconditions
+import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
+import com.android.quickstep.util.TaskKeyByLastActiveTimeCache
+import com.android.quickstep.util.TaskKeyCache
+import com.android.quickstep.util.TaskKeyLruCache
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+class TaskThumbnailCache
+@VisibleForTesting
+internal constructor(
+    private val context: Context,
+    private val bgExecutor: Executor,
+    private val cache: TaskKeyCache<ThumbnailData>,
+) : TaskThumbnailDataSource {
+    val highResLoadingState = HighResLoadingState()
+    private val enableTaskSnapshotPreloading =
+        context.resources.getBoolean(R.bool.config_enableTaskSnapshotPreloading)
+
+    @JvmOverloads
+    constructor(
+        context: Context,
+        bgExecutor: Executor,
+        cacheSize: Int = context.resources.getInteger(R.integer.recentsThumbnailCacheSize),
+    ) : this(
+        context,
+        bgExecutor,
+        if (enableGridOnlyOverview()) TaskKeyByLastActiveTimeCache(cacheSize)
+        else TaskKeyLruCache(cacheSize),
+    )
+
+    /**
+     * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
+     * puts it in the cache.
+     */
+    fun updateThumbnailInCache(task: Task?, lowResolution: Boolean) {
+        task ?: return
+
+        Preconditions.assertUIThread()
+        // Fetch the thumbnail for this task and put it in the cache
+        if (task.thumbnail == null) {
+            getThumbnailInBackground(task.key, lowResolution) { t: ThumbnailData? ->
+                task.thumbnail = t
+            }
+        }
+    }
+
+    /** Synchronously updates the thumbnail in the cache if it is already there. */
+    fun updateTaskSnapShot(taskId: Int, thumbnail: ThumbnailData?) {
+        Preconditions.assertUIThread()
+        cache.updateIfAlreadyInCache(taskId, thumbnail)
+    }
+
+    // TODO(b/387496731): Add ensureActive() calls if they show performance benefit
+    /**
+     * Retrieves a thumbnail for the provided `task` on the current thread. This should not be
+     * called from the main thread.
+     */
+    @WorkerThread
+    override suspend fun getThumbnail(task: Task): ThumbnailData? {
+        val lowResolution: Boolean = !highResLoadingState.isEnabled
+        // Check task for thumbnail
+        val taskThumbnail: ThumbnailData? = task.thumbnail
+        if (
+            taskThumbnail?.thumbnail != null && (!taskThumbnail.reducedResolution || lowResolution)
+        ) {
+            return taskThumbnail
+        }
+
+        // Check cache for thumbnail
+        val cachedThumbnail: ThumbnailData? = cache.getAndInvalidateIfModified(task.key)
+        if (
+            cachedThumbnail?.thumbnail != null &&
+                (!cachedThumbnail.reducedResolution || lowResolution)
+        ) {
+            return cachedThumbnail
+        }
+
+        // Get thumbnail from system
+        var thumbnailData =
+            ActivityManagerWrapper.getInstance().getTaskThumbnail(task.key.id, lowResolution)
+        if (thumbnailData.thumbnail == null) {
+            thumbnailData = ActivityManagerWrapper.getInstance().takeTaskThumbnail(task.key.id)
+        }
+
+        // Avoid an async timing issue that a low res entry replaces an existing high
+        // res entry in high res enabled state, so we check before putting it to cache
+        if (
+            enableGridOnlyOverview() &&
+                thumbnailData.reducedResolution &&
+                highResLoadingState.isEnabled
+        ) {
+            val newCachedThumbnail = cache.getAndInvalidateIfModified(task.key)
+            if (newCachedThumbnail.thumbnail != null && !newCachedThumbnail.reducedResolution) {
+                return newCachedThumbnail
+            }
+        }
+        cache.put(task.key, thumbnailData)
+        return thumbnailData
+    }
+
+    /**
+     * Asynchronously fetches the thumbnail for the given `task`.
+     *
+     * @param callback The callback to receive the task after its data has been populated.
+     * @return a cancelable handle to the request
+     */
+    fun getThumbnailInBackground(
+        task: Task,
+        callback: Consumer<ThumbnailData>,
+    ): CancellableTask<ThumbnailData>? {
+        Preconditions.assertUIThread()
+
+        val lowResolution = !highResLoadingState.isEnabled
+        val taskThumbnail = task.thumbnail
+        if (
+            taskThumbnail?.thumbnail != null && (!taskThumbnail.reducedResolution || lowResolution)
+        ) {
+            // Nothing to load, the thumbnail is already high-resolution or matches what the
+            // request, so just callback
+            callback.accept(taskThumbnail)
+            return null
+        }
+
+        return getThumbnailInBackground(task.key, !highResLoadingState.isEnabled, callback)
+    }
+
+    /**
+     * Updates cache size and remove excess entries if current size is more than new cache size.
+     *
+     * @return whether cache size has increased
+     */
+    fun updateCacheSizeAndRemoveExcess(): Boolean {
+        val newSize = context.resources.getInteger(R.integer.recentsThumbnailCacheSize)
+        val oldSize = cache.maxSize
+        if (newSize == oldSize) {
+            // Return if no change in size
+            return false
+        }
+
+        cache.updateCacheSizeAndRemoveExcess(newSize)
+        return newSize > oldSize
+    }
+
+    private fun getThumbnailInBackground(
+        key: TaskKey,
+        lowResolution: Boolean,
+        callback: Consumer<ThumbnailData>,
+    ): CancellableTask<ThumbnailData>? {
+        Preconditions.assertUIThread()
+
+        val cachedThumbnail = cache.getAndInvalidateIfModified(key)
+        if (
+            cachedThumbnail?.thumbnail != null &&
+                (!cachedThumbnail.reducedResolution || lowResolution)
+        ) {
+            // Already cached, lets use that thumbnail
+            callback.accept(cachedThumbnail)
+            return null
+        }
+
+        val request =
+            CancellableTask(
+                {
+                    val thumbnailData =
+                        ActivityManagerWrapper.getInstance().getTaskThumbnail(key.id, lowResolution)
+                    if (thumbnailData.thumbnail != null) thumbnailData
+                    else ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id)
+                },
+                Executors.MAIN_EXECUTOR,
+                Consumer { result: ThumbnailData ->
+                    // Avoid an async timing issue that a low res entry replaces an existing high
+                    // res entry in high res enabled state, so we check before putting it to cache
+                    if (
+                        enableGridOnlyOverview() &&
+                            result.reducedResolution &&
+                            highResLoadingState.isEnabled
+                    ) {
+                        val newCachedThumbnail = cache.getAndInvalidateIfModified(key)
+                        if (
+                            newCachedThumbnail?.thumbnail != null &&
+                                !newCachedThumbnail.reducedResolution
+                        ) {
+                            return@Consumer
+                        }
+                    }
+                    cache.put(key, result)
+                    callback.accept(result)
+                },
+            )
+        bgExecutor.execute(request)
+        return request
+    }
+
+    /** Clears the cache. */
+    fun clear() {
+        cache.evictAll()
+    }
+
+    /** Removes the cached thumbnail for the given task. */
+    fun remove(key: TaskKey) {
+        cache.remove(key)
+    }
+
+    /** Returns The cache size. */
+    fun getCacheSize() = cache.maxSize
+
+    /** Returns Whether to enable background preloading of task thumbnails. */
+    fun isPreloadingEnabled() = enableTaskSnapshotPreloading && highResLoadingState.visible
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index daac9fb..44fdaec 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -136,7 +136,7 @@
             setter.add(pa.buildAnim());
         }
 
-        Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
+        Pair<FloatProperty<RecentsView<?, ?>>, FloatProperty<RecentsView<?, ?>>> taskViewsFloat =
                 mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset(
                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
                         mRecentsViewContainer.getDeviceProfile());
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 6b61298..f3f73c0 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -53,7 +53,7 @@
     private final int mTouchSlop;
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
-    private final long mTimeForTap;
+    private final long mTimeForLongPress;
     private int mActivePointerId = INVALID_POINTER_ID;
 
     public BubbleBarInputConsumer(Context context, BubbleControllers bubbleControllers,
@@ -64,7 +64,7 @@
 
         mInputMonitorCompat = inputMonitorCompat;
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
-        mTimeForTap = ViewConfiguration.getTapTimeout();
+        mTimeForLongPress = ViewConfiguration.getLongPressTimeout();
     }
 
     @Override
@@ -110,7 +110,8 @@
             case MotionEvent.ACTION_UP:
                 boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
                         && mBubbleBarSwipeController.isSwipeGesture();
-                boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap;
+                // Anything less than a long-press is a tap
+                boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForLongPress;
                 if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
                         && mStashedOrCollapsedOnDown) {
                     // Taps on the handle / collapsed state should open the bar
diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
index df546ca..ad2bd25 100644
--- a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.recents.data
 
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
 
 /** Notifies added callbacks that high res state has changed */
 interface HighResLoadingStateNotifier {
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
index a45d194..608fafd 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -17,7 +17,7 @@
 package com.android.quickstep.recents.data
 
 import android.os.UserHandle
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
 import com.android.quickstep.util.TaskVisualsChangeListener
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 8a1b211..703d631 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep.recents.data
 
 import android.graphics.drawable.Drawable
+import android.graphics.drawable.ShapeDrawable
 import android.util.Log
 import com.android.launcher3.util.coroutines.DispatcherProvider
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
@@ -25,16 +26,16 @@
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
-import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
 class TasksRepository(
@@ -112,10 +113,11 @@
         taskRequests[taskId] =
             Pair(
                 task.key,
-                recentsCoroutineScope.launch(dispatcherProvider.main) {
+                recentsCoroutineScope.launch(dispatcherProvider.background) {
                     Log.i(TAG, "requestTaskData: $taskId")
-                    fetchIcon(task)
-                    fetchThumbnail(task)
+                    val thumbnailFetchDeferred = async { fetchThumbnail(task) }
+                    val iconFetchDeferred = async { fetchIcon(task) }
+                    awaitAll(thumbnailFetchDeferred, iconFetchDeferred)
                 },
             )
     }
@@ -150,7 +152,7 @@
             task.key,
             object : TaskIconChangedCallback {
                 override fun onTaskIconChanged() {
-                    recentsCoroutineScope.launch(dispatcherProvider.main) {
+                    recentsCoroutineScope.launch(dispatcherProvider.background) {
                         updateIcon(task.key.id, getIconFromDataSource(task))
                     }
                 }
@@ -168,7 +170,7 @@
                 }
 
                 override fun onHighResLoadingStateChanged() {
-                    recentsCoroutineScope.launch(dispatcherProvider.main) {
+                    recentsCoroutineScope.launch(dispatcherProvider.background) {
                         updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
                     }
                 }
@@ -191,34 +193,18 @@
     }
 
     private suspend fun getThumbnailFromDataSource(task: Task) =
-        withContext(dispatcherProvider.main) {
-            suspendCancellableCoroutine { continuation ->
-                val cancellableTask =
-                    taskThumbnailDataSource.getThumbnailInBackground(task) {
-                        continuation.resume(it)
-                    }
-                continuation.invokeOnCancellation { cancellableTask?.cancel() }
-            }
-        }
+        withContext(dispatcherProvider.background) { taskThumbnailDataSource.getThumbnail(task) }
 
     private suspend fun getIconFromDataSource(task: Task) =
-        withContext(dispatcherProvider.main) {
-            suspendCancellableCoroutine { continuation ->
-                val cancellableTask =
-                    taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title
-                        ->
-                        icon.constantState?.let {
-                            continuation.resume(
-                                IconData(it.newDrawable().mutate(), contentDescription, title)
-                            )
-                        }
-                    }
-                continuation.invokeOnCancellation { cancellableTask?.cancel() }
-            }
+        withContext(dispatcherProvider.background) {
+            val iconCacheEntry = taskIconDataSource.getIcon(task)
+            val icon = iconCacheEntry.icon.constantState?.newDrawable()?.mutate() ?: EMPTY_DRAWABLE
+            IconData(icon, iconCacheEntry.contentDescription, iconCacheEntry.title)
         }
 
     companion object {
         private const val TAG = "TasksRepository"
+        private val EMPTY_DRAWABLE = ShapeDrawable()
     }
 
     /** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 9d8fc4f..dd83af6 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -191,6 +191,7 @@
                         recentsViewData = inject(),
                         recentTasksRepository = inject(),
                         getThumbnailPositionUseCase = inject(),
+                        dispatcherProvider = inject(),
                     )
                 }
                 GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
index ab699c6..c45458c 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
@@ -16,10 +16,9 @@
 
 package com.android.quickstep.task.thumbnail.data
 
-import com.android.launcher3.util.CancellableTask
-import com.android.quickstep.TaskIconCache.GetTaskIconCallback
+import com.android.quickstep.TaskIconCache
 import com.android.systemui.shared.recents.model.Task
 
 interface TaskIconDataSource {
-    fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>?
+    suspend fun getIcon(task: Task): TaskIconCache.TaskCacheEntry
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
index 986acbe..6e63ea9 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
@@ -16,14 +16,9 @@
 
 package com.android.quickstep.task.thumbnail.data
 
-import com.android.launcher3.util.CancellableTask
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
-import java.util.function.Consumer
 
 interface TaskThumbnailDataSource {
-    fun getThumbnailInBackground(
-        task: Task,
-        callback: Consumer<ThumbnailData>
-    ): CancellableTask<ThumbnailData>?
+    suspend fun getThumbnail(task: Task): ThumbnailData?
 }
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index e6c8d27..0f61b95 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -74,6 +74,7 @@
                 recentsViewData = RecentsDependencies.get(),
                 getThumbnailPositionUseCase = RecentsDependencies.get(),
                 recentTasksRepository = RecentsDependencies.get(),
+                dispatcherProvider = RecentsDependencies.get(),
             )
         viewModel.overlayState
             .onEach {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 14359db..81a904b 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep.task.viewmodel
 
 import android.graphics.Matrix
+import com.android.launcher3.util.coroutines.DispatcherProvider
 import com.android.quickstep.recents.data.RecentTasksRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
@@ -27,6 +28,7 @@
 import com.android.systemui.shared.recents.model.Task
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
 /** View model for TaskOverlay */
@@ -35,11 +37,14 @@
     recentsViewData: RecentsViewData,
     private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
     recentTasksRepository: RecentTasksRepository,
+    dispatcherProvider: DispatcherProvider,
 ) {
     val overlayState =
         combine(
                 recentsViewData.overlayEnabled,
-                recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
+                recentsViewData.settledFullyVisibleTaskIds
+                    .map { it.contains(task.key.id) }
+                    .distinctUntilChanged(),
                 recentTasksRepository.getThumbnailById(task.key.id),
             ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
                 if (isOverlayEnabled && isFullyVisible) {
@@ -52,6 +57,7 @@
                 }
             }
             .distinctUntilChanged()
+            .flowOn(dispatcherProvider.background)
 
     fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
         val matrix: Matrix
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4660c51..7c745a2 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -182,6 +182,7 @@
 import com.android.launcher3.util.coroutines.DispatcherProvider;
 import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.GestureState;
+import com.android.quickstep.HighResLoadingState;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
@@ -194,7 +195,6 @@
 import com.android.quickstep.SplitSelectionListener;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
@@ -263,14 +263,14 @@
 public abstract class RecentsView<
         CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
-        TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+        HighResLoadingState.HighResLoadingStateChangedCallback,
         TaskVisualsChangeListener {
 
     private static final String TAG = "RecentsView";
     private static final boolean DEBUG = false;
 
-    public static final FloatProperty<RecentsView> CONTENT_ALPHA =
-            new FloatProperty<RecentsView>("contentAlpha") {
+    public static final FloatProperty<RecentsView<?, ?>> CONTENT_ALPHA =
+            new FloatProperty<>("contentAlpha") {
                 @Override
                 public void setValue(RecentsView view, float v) {
                     view.setContentAlpha(v);
@@ -282,8 +282,8 @@
                 }
             };
 
-    public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS =
-            new FloatProperty<RecentsView>("fullscreenProgress") {
+    public static final FloatProperty<RecentsView<?, ?>> FULLSCREEN_PROGRESS =
+            new FloatProperty<>("fullscreenProgress") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     recentsView.setFullscreenProgress(v);
@@ -295,8 +295,8 @@
                 }
             };
 
-    public static final FloatProperty<RecentsView> TASK_MODALNESS =
-            new FloatProperty<RecentsView>("taskModalness") {
+    public static final FloatProperty<RecentsView<?, ?>> TASK_MODALNESS =
+            new FloatProperty<>("taskModalness") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     recentsView.setTaskModalness(v);
@@ -308,8 +308,8 @@
                 }
             };
 
-    public static final FloatProperty<RecentsView> ADJACENT_PAGE_HORIZONTAL_OFFSET =
-            new FloatProperty<RecentsView>("adjacentPageHorizontalOffset") {
+    public static final FloatProperty<RecentsView<?, ?>> ADJACENT_PAGE_HORIZONTAL_OFFSET =
+            new FloatProperty<>("adjacentPageHorizontalOffset") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     if (recentsView.mAdjacentPageHorizontalOffset != v) {
@@ -324,8 +324,8 @@
                 }
             };
 
-    public static final FloatProperty<RecentsView> RUNNING_TASK_ATTACH_ALPHA =
-            new FloatProperty<RecentsView>("runningTaskAttachAlpha") {
+    public static final FloatProperty<RecentsView<?, ?>> RUNNING_TASK_ATTACH_ALPHA =
+            new FloatProperty<>("runningTaskAttachAlpha") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     recentsView.mRunningTaskAttachAlpha = v;
@@ -349,8 +349,8 @@
      * Can be used to tint the color of the RecentsView to simulate a scrim that can views
      * excluded from. Really should be a proper scrim.
      */
-    private static final FloatProperty<RecentsView> COLOR_TINT =
-            new FloatProperty<RecentsView>("colorTint") {
+    private static final FloatProperty<RecentsView<?, ?>> COLOR_TINT =
+            new FloatProperty<>("colorTint") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     recentsView.setColorTint(v);
@@ -368,8 +368,8 @@
      * more specific, we'd want to create a similar FloatProperty just for a TaskView's
      * offsetX/Y property
      */
-    public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION =
-            new FloatProperty<RecentsView>("taskSecondaryTranslation") {
+    public static final FloatProperty<RecentsView<?, ?>> TASK_SECONDARY_TRANSLATION =
+            new FloatProperty<>("taskSecondaryTranslation") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     recentsView.setTaskViewsResistanceTranslation(v);
@@ -387,8 +387,8 @@
      * more specific, we'd want to create a similar FloatProperty just for a TaskView's
      * offsetX/Y property
      */
-    public static final FloatProperty<RecentsView> TASK_PRIMARY_SPLIT_TRANSLATION =
-            new FloatProperty<RecentsView>("taskPrimarySplitTranslation") {
+    public static final FloatProperty<RecentsView<?, ?>> TASK_PRIMARY_SPLIT_TRANSLATION =
+            new FloatProperty<>("taskPrimarySplitTranslation") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     recentsView.setTaskViewsPrimarySplitTranslation(v);
@@ -400,8 +400,8 @@
                 }
             };
 
-    public static final FloatProperty<RecentsView> TASK_SECONDARY_SPLIT_TRANSLATION =
-            new FloatProperty<RecentsView>("taskSecondarySplitTranslation") {
+    public static final FloatProperty<RecentsView<?, ?>> TASK_SECONDARY_SPLIT_TRANSLATION =
+            new FloatProperty<>("taskSecondarySplitTranslation") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
                     recentsView.setTaskViewsSecondarySplitTranslation(v);
@@ -414,8 +414,8 @@
             };
 
     /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
-    public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
-            new FloatProperty<RecentsView>("recentsScale") {
+    public static final FloatProperty<RecentsView<?, ?>> RECENTS_SCALE_PROPERTY =
+            new FloatProperty<>("recentsScale") {
                 @Override
                 public void setValue(RecentsView view, float scale) {
                     view.setScaleX(scale);
@@ -444,8 +444,8 @@
      * Progress of Recents view from carousel layout to grid layout. If Recents is not shown as a
      * grid, then the value remains 0.
      */
-    public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS =
-            new FloatProperty<RecentsView>("recentsGrid") {
+    public static final FloatProperty<RecentsView<?, ?>> RECENTS_GRID_PROGRESS =
+            new FloatProperty<>("recentsGrid") {
                 @Override
                 public void setValue(RecentsView view, float gridProgress) {
                     view.setGridProgress(gridProgress);
@@ -457,7 +457,7 @@
                 }
             };
 
-    public static final FloatProperty<RecentsView> DESKTOP_CAROUSEL_DETACH_PROGRESS =
+    public static final FloatProperty<RecentsView<?, ?>> DESKTOP_CAROUSEL_DETACH_PROGRESS =
             new FloatProperty<>("desktopCarouselDetachProgress") {
                 @Override
                 public void setValue(RecentsView view, float offset) {
@@ -476,8 +476,8 @@
      * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and
      * being in any other state has a value of 0.
      */
-    public static final FloatProperty<RecentsView> TASK_THUMBNAIL_SPLASH_ALPHA =
-            new FloatProperty<RecentsView>("taskThumbnailSplashAlpha") {
+    public static final FloatProperty<RecentsView<?, ?>> TASK_THUMBNAIL_SPLASH_ALPHA =
+            new FloatProperty<>("taskThumbnailSplashAlpha") {
                 @Override
                 public void setValue(RecentsView view, float taskThumbnailSplashAlpha) {
                     view.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
@@ -5484,7 +5484,7 @@
         firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
 
         RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
-        Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
+        Pair<FloatProperty<RecentsView<?, ?>>, FloatProperty<RecentsView<?, ?>>> taskViewsFloat =
                 orientationHandler.getSplitSelectTaskOffset(
                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
                         mContainer.getDeviceProfile());
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 2f773b3..46b5659 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
@@ -245,9 +245,11 @@
             animator.onStashStateChangingWhileAnimating()
         }
 
-        // The physics animation test util posts the cancellation to the looper thread, so we have
-        // to wait again and let it finish.
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // wait for the animation to cancel
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
+            handleAnimator,
+            DynamicAnimation.TRANSLATION_Y,
+        )
 
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
index 7d09efd..4adf01e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.recents.data
 
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
 
 class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier {
     val listeners = mutableListOf<HighResLoadingStateChangedCallback>()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
index 5de876a..f6f158f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -17,11 +17,11 @@
 package com.android.quickstep.recents.data
 
 import android.graphics.drawable.Drawable
-import com.android.launcher3.util.CancellableTask
-import com.android.quickstep.TaskIconCache
+import com.android.quickstep.TaskIconCache.TaskCacheEntry
 import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
 import com.android.systemui.shared.recents.model.Task
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.yield
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -29,28 +29,30 @@
 
     val taskIdToDrawable: MutableMap<Int, Drawable> =
         (0..10).associateWith { mockCopyableDrawable() }.toMutableMap()
-
-    val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
-    var shouldLoadSynchronously: Boolean = true
+    private val completionPrevented: MutableSet<Int> = mutableSetOf()
 
     /** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */
-    override fun getIconInBackground(
-        task: Task,
-        callback: TaskIconCache.GetTaskIconCallback
-    ): CancellableTask<*>? {
-        val wrappedCallback = {
-            callback.onTaskIconReceived(
-                taskIdToDrawable.getValue(task.key.id),
-                "content desc ${task.key.id}",
-                "title ${task.key.id}"
-            )
+    override suspend fun getIcon(task: Task): TaskCacheEntry {
+        while (task.key.id in completionPrevented) {
+            yield()
         }
-        if (shouldLoadSynchronously) {
-            wrappedCallback()
-        } else {
-            taskIdToUpdatingTask[task.key.id] = wrappedCallback
-        }
-        return null
+        return TaskCacheEntry(
+            taskIdToDrawable.getValue(task.key.id),
+            "content desc ${task.key.id}",
+            "title ${task.key.id}",
+        )
+    }
+
+    fun preventIconLoad(taskId: Int) {
+        completionPrevented.add(taskId)
+    }
+
+    fun completeLoadingForTask(taskId: Int) {
+        completionPrevented.remove(taskId)
+    }
+
+    fun completeLoading() {
+        completionPrevented.clear()
     }
 
     companion object {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index d12c0b0..e10afc4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -17,11 +17,10 @@
 package com.android.quickstep.recents.data
 
 import android.graphics.Bitmap
-import com.android.launcher3.util.CancellableTask
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
-import java.util.function.Consumer
+import kotlinx.coroutines.yield
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -29,25 +28,28 @@
 
     val taskIdToBitmap: MutableMap<Int, Bitmap> =
         (0..10).associateWith { mock<Bitmap>() }.toMutableMap()
-    val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
-    var shouldLoadSynchronously: Boolean = true
+    private val completionPrevented: MutableSet<Int> = mutableSetOf()
+    private val getThumbnailCalls = mutableMapOf<Int, Int>()
 
     /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
-    override fun getThumbnailInBackground(
-        task: Task,
-        callback: Consumer<ThumbnailData>
-    ): CancellableTask<ThumbnailData>? {
-        val thumbnailData = mock<ThumbnailData>()
-        whenever(thumbnailData.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
-        val wrappedCallback = {
-            task.thumbnail = thumbnailData
-            callback.accept(thumbnailData)
+    override suspend fun getThumbnail(task: Task): ThumbnailData {
+        getThumbnailCalls[task.key.id] = (getThumbnailCalls[task.key.id] ?: 0) + 1
+
+        while (task.key.id in completionPrevented) {
+            yield()
         }
-        if (shouldLoadSynchronously) {
-            wrappedCallback()
-        } else {
-            taskIdToUpdatingTask[task.key.id] = wrappedCallback
+        return mock<ThumbnailData>().also {
+            whenever(it.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
         }
-        return null
+    }
+
+    fun getNumberOfGetThumbnailCalls(taskId: Int): Int = getThumbnailCalls[taskId] ?: 0
+
+    fun preventThumbnailLoad(taskId: Int) {
+        completionPrevented.add(taskId)
+    }
+
+    fun completeLoadingForTask(taskId: Int) {
+        completionPrevented.remove(taskId)
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index ee1ec6e..b6cf5bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -214,7 +214,7 @@
                 .isEqualTo(bitmap2)
 
             // Prevent new loading of Bitmaps
-            taskThumbnailDataSource.shouldLoadSynchronously = false
+            taskThumbnailDataSource.preventThumbnailLoad(2)
             systemUnderTest.setVisibleTasks(setOf(2, 3))
 
             assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
@@ -235,7 +235,7 @@
                 .assertHasIconDataFromSource(taskIconDataSource)
 
             // Prevent new loading of Drawables
-            taskThumbnailDataSource.shouldLoadSynchronously = false
+            taskIconDataSource.preventIconLoad(2)
             systemUnderTest.setVisibleTasks(setOf(2, 3))
 
             systemUnderTest
@@ -257,9 +257,6 @@
             assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
             task2.assertHasIconDataFromSource(taskIconDataSource)
 
-            // Prevent new loading of Bitmaps
-            taskThumbnailDataSource.shouldLoadSynchronously = false
-            taskIconDataSource.shouldLoadSynchronously = false
             systemUnderTest.setVisibleTasks(setOf(0, 1))
 
             val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
@@ -275,21 +272,22 @@
             // Setup fakes
             recentsModel.seedTasks(defaultTaskList)
             val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-            taskThumbnailDataSource.shouldLoadSynchronously = false
 
             // Setup TasksRepository
             systemUnderTest.getAllTaskData(forceRefresh = true)
-            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
-            // Assert there is no bitmap in first emission
-            assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
+            val task2DataFlow = systemUnderTest.getTaskDataById(2)
+            val task2BitmapValues = mutableListOf<Bitmap?>()
+            testScope.backgroundScope.launch {
+                task2DataFlow.map { it?.thumbnail?.thumbnail }.toList(task2BitmapValues)
+            }
 
-            // Simulate bitmap loading after first emission
-            taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke()
+            // Check for first emission
+            assertThat(task2BitmapValues.single()).isNull()
 
+            systemUnderTest.setVisibleTasks(setOf(2))
             // Check for second emission
-            assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
-                .isEqualTo(bitmap2)
+            assertThat(task2BitmapValues).isEqualTo(listOf(null, bitmap2))
         }
 
     @Test
@@ -365,7 +363,6 @@
         testScope.runTest {
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
-            taskThumbnailDataSource.shouldLoadSynchronously = false
 
             val taskDataFlow = systemUnderTest.getTaskDataById(1)
             val task1IconValues = mutableListOf<Drawable?>()
@@ -374,14 +371,10 @@
             }
 
             systemUnderTest.setVisibleTasks(setOf(1))
-            val task1UpdatingTaskOld = taskThumbnailDataSource.taskIdToUpdatingTask[1]
-            println(task1UpdatingTaskOld)
+            assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1)
 
             systemUnderTest.setVisibleTasks(setOf(1, 2))
-            val task1UpdatingTaskNew = taskThumbnailDataSource.taskIdToUpdatingTask[1]
-            println(task1UpdatingTaskNew)
-
-            assertThat(task1UpdatingTaskNew).isEqualTo(task1UpdatingTaskOld)
+            assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1)
         }
 
     private fun createTaskWithId(taskId: Int) =
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index 2e91f5c..95504af 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -22,6 +22,7 @@
 import android.graphics.Color
 import android.graphics.Matrix
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.TestDispatcherProvider
 import com.android.quickstep.recents.data.FakeTasksRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
@@ -33,7 +34,10 @@
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,6 +45,7 @@
 import org.mockito.kotlin.whenever
 
 /** Test for [TaskOverlayViewModel] */
+@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 class TaskOverlayViewModelTest {
     private val task =
@@ -58,104 +63,123 @@
     private val recentsViewData = RecentsViewData()
     private val tasksRepository = FakeTasksRepository()
     private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(dispatcher)
     private val systemUnderTest =
-        TaskOverlayViewModel(task, recentsViewData, mGetThumbnailPositionUseCase, tasksRepository)
+        TaskOverlayViewModel(
+            task,
+            recentsViewData,
+            mGetThumbnailPositionUseCase,
+            tasksRepository,
+            TestDispatcherProvider(dispatcher),
+        )
 
     @Test
-    fun initialStateIsDisabled() = runTest {
-        assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
-    }
+    fun initialStateIsDisabled() =
+        testScope.runTest { assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) }
 
     @Test
-    fun recentsViewOverlayDisabled_Disabled() = runTest {
-        recentsViewData.overlayEnabled.value = false
-        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+    fun recentsViewOverlayDisabled_Disabled() =
+        testScope.runTest {
+            recentsViewData.overlayEnabled.value = false
+            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
 
-        assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
-    }
+            assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+        }
 
     @Test
-    fun taskNotFullyVisible_Disabled() = runTest {
-        recentsViewData.overlayEnabled.value = true
-        recentsViewData.settledFullyVisibleTaskIds.value = setOf()
+    fun taskNotFullyVisible_Disabled() =
+        testScope.runTest {
+            recentsViewData.overlayEnabled.value = true
+            recentsViewData.settledFullyVisibleTaskIds.value = setOf()
 
-        assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
-    }
+            assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+        }
 
     @Test
-    fun noThumbnail_Enabled() = runTest {
-        recentsViewData.overlayEnabled.value = true
-        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-        task.isLocked = false
+    fun noThumbnail_Enabled() =
+        testScope.runTest {
+            recentsViewData.overlayEnabled.value = true
+            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+            task.isLocked = false
 
-        assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
-    }
+            assertThat(systemUnderTest.overlayState.first())
+                .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
+        }
 
     @Test
-    fun withThumbnail_RealSnapshot_NotLocked_Enabled() = runTest {
-        recentsViewData.overlayEnabled.value = true
-        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(setOf(TASK_ID))
-        thumbnailData.isRealSnapshot = true
-        task.isLocked = false
+    fun withThumbnail_RealSnapshot_NotLocked_Enabled() =
+        testScope.runTest {
+            recentsViewData.overlayEnabled.value = true
+            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+            tasksRepository.seedTasks(listOf(task))
+            tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+            tasksRepository.setVisibleTasks(setOf(TASK_ID))
+            thumbnailData.isRealSnapshot = true
+            task.isLocked = false
 
-        assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
-    }
+            assertThat(systemUnderTest.overlayState.first())
+                .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
+        }
 
     @Test
-    fun withThumbnail_RealSnapshot_Locked_Enabled() = runTest {
-        recentsViewData.overlayEnabled.value = true
-        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(setOf(TASK_ID))
-        thumbnailData.isRealSnapshot = true
-        task.isLocked = true
+    fun withThumbnail_RealSnapshot_Locked_Enabled() =
+        testScope.runTest {
+            recentsViewData.overlayEnabled.value = true
+            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+            tasksRepository.seedTasks(listOf(task))
+            tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+            tasksRepository.setVisibleTasks(setOf(TASK_ID))
+            thumbnailData.isRealSnapshot = true
+            task.isLocked = true
 
-        assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
-    }
+            assertThat(systemUnderTest.overlayState.first())
+                .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
+        }
 
     @Test
-    fun withThumbnail_FakeSnapshot_Enabled() = runTest {
-        recentsViewData.overlayEnabled.value = true
-        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(setOf(TASK_ID))
-        thumbnailData.isRealSnapshot = false
-        task.isLocked = false
+    fun withThumbnail_FakeSnapshot_Enabled() =
+        testScope.runTest {
+            recentsViewData.overlayEnabled.value = true
+            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+            tasksRepository.seedTasks(listOf(task))
+            tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+            tasksRepository.setVisibleTasks(setOf(TASK_ID))
+            thumbnailData.isRealSnapshot = false
+            task.isLocked = false
 
-        assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
-    }
+            assertThat(systemUnderTest.overlayState.first())
+                .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
+        }
 
     @Test
-    fun getThumbnailMatrix_MissingThumbnail() = runTest {
-        val isRtl = true
+    fun getThumbnailMatrix_MissingThumbnail() =
+        testScope.runTest {
+            val isRtl = true
 
-        whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .thenReturn(MissingThumbnail)
+            whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+                .thenReturn(MissingThumbnail)
 
-        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
-    }
+            assertThat(
+                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+                )
+                .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
+        }
 
     @Test
-    fun getThumbnailMatrix_MatrixScaling() = runTest {
-        val isRtl = true
-        val isRotated = true
+    fun getThumbnailMatrix_MatrixScaling() =
+        testScope.runTest {
+            val isRtl = true
+            val isRotated = true
 
-        whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .thenReturn(MatrixScaling(MATRIX, isRotated))
+            whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+                .thenReturn(MatrixScaling(MATRIX, isRotated))
 
-        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
-    }
+            assertThat(
+                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+                )
+                .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
+        }
 
     companion object {
         const val TASK_ID = 0
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
index 648fa93..ef4591e 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
@@ -44,7 +44,6 @@
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -67,7 +66,7 @@
     private RecentTasksList mTasksList;
 
     @Mock
-    private TaskThumbnailCache.HighResLoadingState mHighResLoadingState;
+    private HighResLoadingState mHighResLoadingState;
 
     private RecentsModel mRecentsModel;
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index ec6a9c3..961dc58 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -337,7 +337,6 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @ScreenRecord // b/313464374
     @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
     public void testQuickSwitchFromApp() throws Exception {
         startTestActivity(2);
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6f293b6..501e650 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -308,6 +308,8 @@
     <string name="folder_name_format_exact">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> items</string>
     <!-- Folder name format when folder has 4 or more items shown in preview-->
     <string name="folder_name_format_overflow">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> or more items</string>
+    <!-- Accessibility announement for unnamed folders -->
+    <string name="unnamed_folder">Unnamed folder</string>
 
     <!-- App pair accessibility -->
     <!-- App pair name -->
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 61297ce..da73280 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -367,27 +367,9 @@
 
     @UiThread
     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
-        applyFromWorkspaceItem(info, /* animate = */ false, /* staggerIndex = */ 0);
-    }
-
-    @UiThread
-    public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
         applyFromWorkspaceItem(info, null);
     }
 
-    /**
-     * Returns whether the newInfo differs from the current getTag().
-     */
-    public boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
-        WorkspaceItemInfo oldInfo = getTag() instanceof WorkspaceItemInfo
-                ? (WorkspaceItemInfo) getTag()
-                : null;
-        boolean changedIcons = oldInfo != null && oldInfo.getTargetComponent() != null
-                && newInfo.getTargetComponent() != null
-                && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
-        return changedIcons && isShown();
-    }
-
     @Override
     public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
         if (delegate instanceof BaseAccessibilityDelegate) {
@@ -1326,7 +1308,6 @@
                 applyFromApplicationInfo((AppInfo) info);
             } else if (info instanceof WorkspaceItemInfo) {
                 applyFromWorkspaceItem((WorkspaceItemInfo) info);
-                mActivity.invalidateParent(info);
             } else if (info != null) {
                 applyFromItemInfoWithIcon(info);
             }
@@ -1340,12 +1321,11 @@
      * Verifies that the current icon is high-res otherwise posts a request to load the icon.
      */
     public void verifyHighRes() {
-        if (mIconLoadRequest != null) {
-            mIconLoadRequest.cancel();
-            mIconLoadRequest = null;
-        }
         if (getTag() instanceof ItemInfoWithIcon info && !mHighResUpdateInProgress
                 && info.getMatchingLookupFlag().useLowRes()) {
+            if (mIconLoadRequest != null) {
+                mIconLoadRequest.cancel();
+            }
             mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
                     .updateIconInBackground(BubbleTextView.this, info);
         }
diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt
index 4d3fe52..66c948a 100644
--- a/src/com/android/launcher3/DropTargetHandler.kt
+++ b/src/com/android/launcher3/DropTargetHandler.kt
@@ -66,6 +66,11 @@
 
     fun onDeleteComplete(item: ItemInfo) {
         removeItemAndStripEmptyScreens(null /* view */, item)
+        AbstractFloatingView.closeOpenViews(
+            mLauncher,
+            false,
+            AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME,
+        )
         var pageItem: ItemInfo = item
         if (item.container <= 0) {
             val v = mLauncher.workspace.getHomescreenIconByItemId(item.container)
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 3fbf888..4097dca 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -72,7 +72,6 @@
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
-import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW;
 import static com.android.launcher3.logging.StatsLogManager.EventEnum;
@@ -206,7 +205,6 @@
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -829,26 +827,6 @@
         return true;
     }
 
-    @Override
-    public void invalidateParent(ItemInfo info) {
-        if (info.container >= 0) {
-            View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container);
-            if (collectionIcon instanceof FolderIcon folderIcon
-                    && collectionIcon.getTag() instanceof FolderInfo) {
-                if (createFolderGridOrganizer(getDeviceProfile())
-                        .setFolderInfo((FolderInfo) folderIcon.getTag())
-                        .isItemInPreview(info.rank)) {
-                    folderIcon.invalidate();
-                }
-            } else if (collectionIcon instanceof AppPairIcon appPairIcon
-                    && collectionIcon.getTag() instanceof AppPairInfo appPairInfo) {
-                if (appPairInfo.getContents().contains(info)) {
-                    appPairIcon.getIconDrawableArea().redraw();
-                }
-            }
-        }
-    }
-
     /**
      * Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have
      * a configuration step, this allows the proper animations to run after other transitions.
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 81d6631..78b53a9 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -199,6 +199,8 @@
                 host.requestFocus();
                 host.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
                 host.performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null);
+                AbstractFloatingView.closeOpenViews(mContext, /* animate= */ false,
+                        AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME);
             });
             return true;
         } else if (action == DEEP_SHORTCUTS) {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index de1bcc3..2481a1a 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -816,6 +816,10 @@
      * Returns a formatted accessibility title for folder
      */
     public String getAccessiblityTitle(CharSequence title) {
+        if (title == null) {
+            // Avoids "Talkback -> Folder: null" announcement.
+            title = getContext().getString(R.string.unnamed_folder);
+        }
         int size = mInfo.getContents().size();
         if (size < MAX_NUM_ITEMS_IN_PREVIEW) {
             return getContext().getString(R.string.folder_name_format_exact, title, size);
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 8d751e6..22f1164 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -24,7 +24,6 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Path;
-import android.graphics.drawable.Drawable;
 import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -541,19 +540,10 @@
             ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
             for (int i = parent.getChildCount() - 1; i >= 0; i--) {
                 View iconView = parent.getChildAt(i);
-                Drawable d = null;
                 if (iconView instanceof BubbleTextView btv) {
                     btv.verifyHighRes();
-                    d = btv.getIcon();
                 } else if (iconView instanceof AppPairIcon api) {
                     api.verifyHighRes();
-                    d = api.getIconDrawableArea().getDrawable();
-                }
-
-                // Set the callback back to the actual icon, in case
-                // it was captured by the FolderIcon
-                if (d != null) {
-                    d.setCallback(iconView);
                 }
             }
         }
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 2276ac7..5ee6a25 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.folder;
 
 import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
+import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
@@ -43,12 +44,14 @@
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.apppairs.AppPairIconDrawingParams;
 import com.android.launcher3.apppairs.AppPairIconGraphic;
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
@@ -460,6 +463,18 @@
         // Set the callback to FolderIcon as it is responsible to drawing the icon. The
         // callback will be released when the folder is opened.
         p.drawable.setCallback(mIcon);
+
+        // Verify high res
+        if (item instanceof ItemInfoWithIcon info
+                && info.getMatchingLookupFlag().isVisuallyLessThan(DESKTOP_ICON_FLAG)) {
+            LauncherAppState.getInstance(mContext).getIconCache().updateIconInBackground(
+                    newInfo -> {
+                        if (p.item == newInfo) {
+                            setDrawable(p, newInfo);
+                            mIcon.invalidate();
+                        }
+                    }, info);
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index b8481c5..b164b7f 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -105,13 +105,6 @@
         return null;
     }
 
-    /**
-     * For items with tree hierarchy, notifies the activity to invalidate the parent when a root
-     * is invalidated
-     * @param info info associated with a root node.
-     */
-    default void invalidateParent(ItemInfo info) { }
-
     default AccessibilityDelegate getAccessibilityDelegate() {
         return null;
     }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index b877d7a..94ae69c 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -1,5 +1,7 @@
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
+
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
@@ -14,6 +16,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.icons.cache.BaseIconCache;
@@ -108,6 +111,13 @@
 
         Point cellSize = new Point();
         for (DeviceProfile dp : idp.supportedProfiles) {
+            // On phones we no longer support regular landscape, only fixed landscape for this
+            // reason we don't need to take regular landscape into account in phones
+            if (Flags.oneGridSpecs() && dp.inv.deviceType == TYPE_PHONE
+                    && dp.inv.isFixedLandscape != dp.isLandscape) {
+                continue;
+            }
+
             dp.getCellSize(cellSize);
             Rect widgetPadding = dp.widgetPadding;
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
index 8952b85..2e556e8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt
@@ -80,6 +80,15 @@
             )
     }
 
+    /**
+     * Sets the test app for app icons to the specified Component
+     *
+     * @param testAppComponent ComponentName to use for app icons
+     */
+    fun setTestAppComponent(testAppComponent: ComponentName) {
+        appComponentName = testAppComponent
+    }
+
     private fun addCorrespondingWidgetRect(
         widgetRect: WidgetRect,
         transaction: FavoriteItemsTransaction,
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index 7f481b7..548cf5b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -17,21 +17,22 @@
 package com.android.launcher3.folder
 
 import android.R
-import android.content.Context
 import android.graphics.Bitmap
 import android.os.Process
-import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
 import com.android.launcher3.LauncherPrefs.Companion.get
 import com.android.launcher3.graphics.PreloadIconDrawable
+import com.android.launcher3.icons.BitmapInfo
 import com.android.launcher3.icons.FastBitmapDrawable
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver
+import com.android.launcher3.icons.PlaceHolderIconDrawable
 import com.android.launcher3.icons.UserBadgeDrawable
 import com.android.launcher3.icons.mono.MonoThemedBitmap
 import com.android.launcher3.model.data.FolderInfo
-import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED
 import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE
 import com.android.launcher3.model.data.WorkspaceItemInfo
@@ -40,6 +41,7 @@
 import com.android.launcher3.util.FlagOp
 import com.android.launcher3.util.LauncherLayoutBuilder
 import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.TestUtil
 import com.android.launcher3.util.UserIconInfo
 import com.google.common.truth.Truth.assertThat
@@ -47,6 +49,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 /** Tests for [PreviewItemManager] */
 @SmallTest
@@ -54,22 +63,27 @@
 class PreviewItemManagerTest {
 
     private lateinit var previewItemManager: PreviewItemManager
-    private lateinit var context: Context
-    private lateinit var folderItems: ArrayList<ItemInfo>
+    private lateinit var context: SandboxModelContext
+    private lateinit var folderItems: ArrayList<WorkspaceItemInfo>
     private lateinit var modelHelper: LauncherModelHelper
     private lateinit var folderIcon: FolderIcon
+    private lateinit var iconCache: IconCache
 
     private var defaultThemedIcons = false
 
     @Before
     fun setup() {
-        getInstrumentation().runOnMainSync {
-            folderIcon =
-                FolderIcon(ActivityContextWrapper(ApplicationProvider.getApplicationContext()))
-        }
-        context = getInstrumentation().targetContext
-        previewItemManager = PreviewItemManager(folderIcon)
         modelHelper = LauncherModelHelper()
+        context = modelHelper.sandboxContext
+        folderIcon = FolderIcon(ActivityContextWrapper(context))
+
+        val app = spy(LauncherAppState.getInstance(context))
+        iconCache = spy(app.iconCache)
+        doReturn(iconCache).whenever(app).iconCache
+        context.putObject(LauncherAppState.INSTANCE, app)
+        doReturn(null).whenever(iconCache).updateIconInBackground(any(), any())
+
+        previewItemManager = PreviewItemManager(folderIcon)
         modelHelper
             .setupDefaultLayoutProvider(
                 LauncherLayoutBuilder()
@@ -82,33 +96,35 @@
                     .build()
             )
             .loadModelSync()
-        folderItems = modelHelper.bgDataModel.collections.valueAt(0).getContents()
+
+        // Use getAppContents() to "cast" contents to WorkspaceItemInfo so we can set bitmaps
+        folderItems = modelHelper.bgDataModel.collections.valueAt(0).getAppContents()
         folderIcon.mInfo = modelHelper.bgDataModel.collections.valueAt(0) as FolderInfo
         folderIcon.mInfo.getContents().addAll(folderItems)
 
-        // Use getAppContents() to "cast" contents to WorkspaceItemInfo so we can set bitmaps
-        val folderApps = modelHelper.bgDataModel.collections.valueAt(0).getAppContents()
         // Set first icon to be themed.
-        folderApps[0].bitmap.themedBitmap =
+        folderItems[0].bitmap.themedBitmap =
             MonoThemedBitmap(
-                folderApps[0].bitmap.icon,
+                folderItems[0].bitmap.icon,
                 Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
             )
 
         // Set second icon to be non-themed.
-        folderApps[1].bitmap.themedBitmap = null
+        folderItems[1].bitmap.themedBitmap = null
 
         // Set third icon to be themed with badge.
-        folderApps[2].bitmap.themedBitmap =
+        folderItems[2].bitmap.themedBitmap =
             MonoThemedBitmap(
-                folderApps[2].bitmap.icon,
+                folderItems[2].bitmap.icon,
                 Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
             )
-        folderApps[2].bitmap = folderApps[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
+        folderItems[2].bitmap =
+            folderItems[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
 
         // Set fourth icon to be non-themed with badge.
-        folderApps[3].bitmap = folderApps[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
-        folderApps[3].bitmap.themedBitmap = null
+        folderItems[3].bitmap =
+            folderItems[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
+        folderItems[3].bitmap.themedBitmap = null
 
         defaultThemedIcons = get(context).get(THEMED_ICONS)
     }
@@ -232,6 +248,31 @@
         assertThat(drawingParams.drawable).isInstanceOf(PreloadIconDrawable::class.java)
     }
 
+    @Test
+    fun `Preview item loads and apply high res icon`() {
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+        val originalBitmap = folderItems[3].bitmap
+        folderItems[3].bitmap = BitmapInfo.LOW_RES_INFO
+
+        previewItemManager.setDrawable(drawingParams, folderItems[3])
+        assertThat(drawingParams.drawable).isInstanceOf(PlaceHolderIconDrawable::class.java)
+
+        val callbackCaptor = argumentCaptor<ItemInfoUpdateReceiver>()
+        verify(iconCache).updateIconInBackground(callbackCaptor.capture(), eq(folderItems[3]))
+
+        // Restore high-res icon
+        folderItems[3].bitmap = originalBitmap
+
+        // Calling with a different item info will ignore the update
+        callbackCaptor.firstValue.reapplyItemInfo(folderItems[2])
+        assertThat(drawingParams.drawable).isInstanceOf(PlaceHolderIconDrawable::class.java)
+
+        // Calling with correct value will update the drawable to high-res
+        callbackCaptor.firstValue.reapplyItemInfo(folderItems[3])
+        assertThat(drawingParams.drawable).isNotInstanceOf(PlaceHolderIconDrawable::class.java)
+        assertThat(drawingParams.drawable).isInstanceOf(FastBitmapDrawable::class.java)
+    }
+
     private fun profileFlagOp(type: Int) =
         UserIconInfo(Process.myUserHandle(), type).applyBitmapInfoFlags(FlagOp.NO_OP)
 }