Merge "(1/2) Fix NPE for title description broken by ag/28991306. It can be nullable initially before its set." into main
diff --git a/Android.bp b/Android.bp
index def024e..b205d0c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -27,7 +27,7 @@
         "android.os.flags-aconfig-java",
         "android.appwidget.flags-aconfig-java",
         "com.android.window.flags.window-aconfig-java",
-    ]
+    ],
 }
 
 // Common source files used to build launcher (java and kotlin)
@@ -153,7 +153,7 @@
     soong_config_variables: {
         release_enable_compose_in_launcher: {
             srcs: [
-                ":launcher-compose-enabled-src"
+                ":launcher-compose-enabled-src",
             ],
 
             // Compose dependencies
@@ -166,7 +166,7 @@
             // in compose/launcher3/facade/disabled/.
             conditions_default: {
                 srcs: [
-                    ":launcher-compose-disabled-src"
+                    ":launcher-compose-disabled-src",
                 ],
                 static_libs: [],
             },
@@ -179,7 +179,7 @@
     soong_config_variables: {
         release_enable_compose_in_launcher: {
             srcs: [
-                ":launcher-quickstep-compose-enabled-src"
+                ":launcher-quickstep-compose-enabled-src",
             ],
 
             // Compose dependencies
@@ -192,7 +192,7 @@
             // in compose/quickstep/facade/disabled/.
             conditions_default: {
                 srcs: [
-                    ":launcher-quickstep-compose-disabled-src"
+                    ":launcher-quickstep-compose-disabled-src",
                 ],
                 static_libs: [],
             },
@@ -322,6 +322,8 @@
         "kotlinx_coroutines",
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
+        "dagger2",
+        "jsr330",
 
     ],
     manifest: "AndroidManifest-common.xml",
@@ -357,6 +359,7 @@
     sdk_version: "current",
     min_sdk_version: min_launcher3_sdk_version,
     target_sdk_version: "current",
+    plugins: ["dagger2-compiler"],
     privileged: true,
     system_ext_specific: true,
 
@@ -392,6 +395,7 @@
         "lottie",
         "SystemUISharedLib",
         "SettingsLibSettingsTheme",
+        "dagger2",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     min_sdk_version: "current",
@@ -421,7 +425,10 @@
         "QuickstepResLib",
         "androidx.room_room-runtime",
     ],
-    plugins: ["androidx.room_room-compiler-plugin"],
+    plugins: [
+        "androidx.room_room-compiler-plugin",
+        "dagger2-compiler",
+    ],
     manifest: "quickstep/AndroidManifest.xml",
     additional_manifests: [
         "go/AndroidManifest.xml",
@@ -437,7 +444,7 @@
     name: "Launcher3QuickStepLib",
     defaults: [
         "launcher_compose_defaults",
-        "quickstep_compose_defaults"
+        "quickstep_compose_defaults",
     ],
     srcs: [
         ":launcher-src",
@@ -458,6 +465,7 @@
     ],
     manifest: "quickstep/AndroidManifest.xml",
     platform_apis: true,
+    plugins: ["dagger2-compiler"],
     min_sdk_version: "current",
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
@@ -500,7 +508,6 @@
 
 }
 
-
 // Build rule for Launcher3 Go app with quickstep for Android Go devices.
 // Note that the following two rules are exactly same, and should
 // eventually be merged into a single target
@@ -540,6 +547,7 @@
         include_filter: ["com.android.launcher3.*"],
     },
 }
+
 android_app {
     name: "Launcher3QuickStepGo",
     static_libs: ["Launcher3GoLib"],
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index dbcf49c..40c3797 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -316,6 +316,7 @@
     description: "Menu in Taskbar with options to launch and manage multiple instances of the same app"
     bug: "355237285"
 }
+
 flag {
     name: "navigate_to_child_preference"
     namespace: "launcher"
@@ -349,3 +350,13 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "ignore_three_finger_trackpad_for_nav_handle_long_press"
+    namespace: "launcher"
+    description: "Ignore three finger trackpad event for nav handle long press"
+    bug: "342143522"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/dagger/LauncherAppComponent.java b/quickstep/src/com/android/launcher3/dagger/LauncherAppComponent.java
new file mode 100644
index 0000000..dab2582
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/dagger/LauncherAppComponent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.dagger;
+
+import dagger.Component;
+
+import javax.inject.Singleton;
+
+/**
+ * Root component for Dagger injection for Launcher Quickstep.
+ */
+@Singleton
+@Component
+public interface LauncherAppComponent extends LauncherBaseAppComponent {
+    /** Builder for quickstep LauncherAppComponent. */
+    @Component.Builder
+    interface Builder extends LauncherBaseAppComponent.Builder {
+        LauncherAppComponent build();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 644705b..e31b1d4 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -36,10 +36,10 @@
 import com.android.quickstep.GestureState;
 import com.android.quickstep.SystemUiProxy;
 import com.android.wm.shell.desktopmode.IDesktopTaskListener;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.util.HashSet;
 import java.util.Set;
-import java.util.concurrent.Executor;
 
 /**
  * Controls the visibility of the workspace and the resumed / paused state when desktop mode
@@ -51,6 +51,7 @@
     private static final boolean DEBUG = false;
     private final Launcher mLauncher;
     private final Set<DesktopVisibilityListener> mDesktopVisibilityListeners = new HashSet<>();
+    private final Set<TaskbarDesktopModeListener> mTaskbarDesktopModeListeners = new HashSet<>();
 
     private int mVisibleDesktopTasksCount;
     private boolean mInOverviewState;
@@ -110,6 +111,16 @@
         mDesktopVisibilityListeners.remove(listener);
     }
 
+    /** Registers a listener for Taskbar changes in Desktop Mode. */
+    public void registerTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) {
+        mTaskbarDesktopModeListeners.add(listener);
+    }
+
+    /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */
+    public void unregisterTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) {
+        mTaskbarDesktopModeListeners.remove(listener);
+    }
+
     /**
      * Sets the number of desktop windows that are visible and updates launcher visibility based on
      * it.
@@ -201,6 +212,16 @@
         DisplayController.handleInfoChangeForDesktopMode(mLauncher);
     }
 
+    private void notifyTaskbarDesktopModeListeners(boolean doesAnyTaskRequireTaskbarRounding) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyTaskbarDesktopModeListeners: doesAnyTaskRequireTaskbarRounding="
+                    + doesAnyTaskRequireTaskbarRounding);
+        }
+        for (TaskbarDesktopModeListener listener : mTaskbarDesktopModeListeners) {
+            listener.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding);
+        }
+    }
+
     /**
      * TODO: b/333533253 - Remove after flag rollout
      */
@@ -377,7 +398,29 @@
 
         @Override
         public void onStashedChanged(int displayId, boolean stashed) {
-            Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated");
+            Log.w(TAG, "DesktopTaskListenerImpl: onStashedChanged is deprecated");
         }
+
+        @Override
+        public void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding) {
+            MAIN_EXECUTOR.execute(() -> {
+                if (mController != null && DesktopModeStatus.useRoundedCorners()) {
+                    Log.d(TAG, "DesktopTaskListenerImpl: doesAnyTaskRequireTaskbarRounding= "
+                            + doesAnyTaskRequireTaskbarRounding);
+                    mController.notifyTaskbarDesktopModeListeners(
+                            doesAnyTaskRequireTaskbarRounding);
+                }
+            });
+        }
+    }
+
+    /** A listener for Taskbar in Desktop Mode. */
+    public interface TaskbarDesktopModeListener {
+        /**
+         * Callback for when task is resized in desktop mode.
+         *
+         * @param doesAnyTaskRequireTaskbarRounding whether task requires taskbar corner roundness.
+         */
+        void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index f61840a..a979d58 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -63,7 +63,6 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.Region.Op;
-import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PaintDrawable;
 import android.graphics.drawable.RotateDrawable;
@@ -74,8 +73,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
-import android.view.View.OnClickListener;
-import android.view.View.OnHoverListener;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
@@ -106,7 +103,6 @@
 import com.android.systemui.shared.navigationbar.KeyButtonRipple;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
 import com.android.systemui.shared.rotation.RotationButton;
-import com.android.systemui.shared.rotation.RotationButtonController;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -304,8 +300,13 @@
                         .get(ALPHA_INDEX_SMALL_SCREEN),
                 flags -> (flags & FLAG_SMALL_SCREEN) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
-                .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        if (!mContext.isPhoneMode()) {
+            mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
+                    .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        }
+
+        // Start at 1 because relevant flags are unset at init.
+        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
 
         // Force nav buttons (specifically back button) to be visible during setup wizard.
         boolean isInSetup = !mContext.isUserSetupComplete();
@@ -317,39 +318,41 @@
         // - IME is showing (add separate translation for IME)
         // - VoiceInteractionWindow (assistant) is showing
         // - Keyboard shortcuts helper is showing
-        int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
-                | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
-        mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
-                flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
-                1, 0));
-        // Center nav buttons in new height for IME.
-        float transForIme = (mContext.getDeviceProfile().taskbarHeight
-                - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
-        // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
-        float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
-        mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
-                flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
-                transForIme, defaultButtonTransY));
+        if (!mContext.isPhoneMode()) {
+            int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
+                    | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
+            mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
+                    flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
+                    1, 0));
+            // Center nav buttons in new height for IME.
+            float transForIme = (mContext.getDeviceProfile().taskbarHeight
+                    - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
+            // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
+            float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
+            mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
+                    flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
+                    transForIme, defaultButtonTransY));
 
-        // Start at 1 because relevant flags are unset at init.
-        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
-        mPropertyHolders.add(new StatePropertyHolder(
-                mOnBackgroundNavButtonColorOverrideMultiplier,
-                flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mOnBackgroundNavButtonColorOverrideMultiplier,
+                    flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(
-                mSlideInViewVisibleNavButtonColorOverride,
-                flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mSlideInViewVisibleNavButtonColorOverride,
+                    flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+        }
 
         if (alwaysShowButtons) {
             initButtons(mNavButtonContainer, mEndContextualContainer,
                     mControllers.navButtonController);
             updateButtonLayoutSpacing();
-            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneButtonNavMode());
+            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneMode());
 
-            mPropertyHolders.add(new StatePropertyHolder(
-                    mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
-                    flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            if (!mContext.isPhoneMode()) {
+                mPropertyHolders.add(new StatePropertyHolder(
+                        mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
+                        flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            }
         } else if (!mIsImeRenderingNavButtons) {
             View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
                     mStartContextualContainer, mControllers.navButtonController, R.id.back);
@@ -711,7 +714,7 @@
     private void applyState() {
         int count = mPropertyHolders.size();
         for (int i = 0; i < count; i++) {
-            mPropertyHolders.get(i).setState(mState);
+            mPropertyHolders.get(i).setState(mState, mContext.isGestureNav());
         }
     }
 
@@ -1177,83 +1180,6 @@
         }
     }
 
-    private class RotationButtonImpl implements RotationButton {
-
-        private final ImageView mButton;
-        private AnimatedVectorDrawable mImageDrawable;
-
-        RotationButtonImpl(ImageView button) {
-            mButton = button;
-        }
-
-        @Override
-        public void setRotationButtonController(RotationButtonController rotationButtonController) {
-            // TODO(b/187754252) UI polish, different icons based on light/dark context, etc
-            mImageDrawable = (AnimatedVectorDrawable) mButton.getContext()
-                    .getDrawable(rotationButtonController.getIconResId());
-            mButton.setImageDrawable(mImageDrawable);
-            mButton.setContentDescription(mButton.getResources()
-                    .getString(R.string.accessibility_rotate_button));
-            mImageDrawable.setCallback(mButton);
-        }
-
-        @Override
-        public View getCurrentView() {
-            return mButton;
-        }
-
-        @Override
-        public boolean show() {
-            mButton.setVisibility(View.VISIBLE);
-            mState |= FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean hide() {
-            mButton.setVisibility(View.GONE);
-            mState &= ~FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean isVisible() {
-            return mButton.getVisibility() == View.VISIBLE;
-        }
-
-        @Override
-        public void updateIcon(int lightIconColor, int darkIconColor) {
-            // TODO(b/187754252): UI Polish
-        }
-
-        @Override
-        public void setOnClickListener(OnClickListener onClickListener) {
-            mButton.setOnClickListener(onClickListener);
-        }
-
-        @Override
-        public void setOnHoverListener(OnHoverListener onHoverListener) {
-            mButton.setOnHoverListener(onHoverListener);
-        }
-
-        @Override
-        public AnimatedVectorDrawable getImageDrawable() {
-            return mImageDrawable;
-        }
-
-        @Override
-        public void setDarkIntensity(float darkIntensity) {
-            // TODO(b/187754252) UI polish
-        }
-
-        @Override
-        public boolean acceptRotationProposal() {
-            return mButton.isAttachedToWindow();
-        }
-    }
-
     private static class StatePropertyHolder {
 
         private final float mEnabledValue, mDisabledValue;
@@ -1284,13 +1210,16 @@
             mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue);
         }
 
-        public void setState(int flags) {
+        public void setState(int flags, boolean skipAnimation) {
             boolean isEnabled = mEnableCondition.test(flags);
             if (mIsEnabled != isEnabled) {
                 mIsEnabled = isEnabled;
                 mAnimator.cancel();
                 mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue);
                 mAnimator.start();
+                if (skipAnimation) {
+                    mAnimator.end();
+                }
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 487bc54..0c1235c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -348,8 +348,9 @@
                 new KeyboardQuickSwitchController(),
                 new TaskbarPinningController(this, () ->
                         DisplayController.isInDesktopMode(this)),
-                bubbleControllersOptional);
-
+                bubbleControllersOptional,
+                new TaskbarDesktopModeController(
+                        LauncherActivityInterface.INSTANCE::getDesktopVisibilityController));
         mLauncherPrefs = LauncherPrefs.get(this);
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index ee64060..4ac7514 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -24,6 +24,7 @@
 import android.graphics.Rect
 import android.graphics.RectF
 import com.android.app.animation.Interpolators
+import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.Utilities.mapRange
@@ -70,8 +71,8 @@
     private var keyShadowDistance = 0f
     private var bottomMargin = 0
 
-    private val fullCornerRadius = context.cornerRadius.toFloat()
-    private var cornerRadius = fullCornerRadius
+    private val fullCornerRadius: Float
+    private var cornerRadius = 0f
     private var widthInsetPercentage = 0f
     private val square = Path()
     private val circle = Path()
@@ -101,7 +102,14 @@
             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
         }
 
-        setCornerRoundness(DEFAULT_ROUNDNESS)
+        if (DisplayController.isInDesktopMode(context)) {
+            fullCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+            cornerRadius = fullCornerRadius
+        } else {
+            fullCornerRadius = context.cornerRadius.toFloat()
+            cornerRadius = fullCornerRadius
+            setCornerRoundness(MAX_ROUNDNESS)
+        }
     }
 
     fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) {
@@ -273,7 +281,7 @@
     }
 
     companion object {
-        const val DEFAULT_ROUNDNESS = 1f
+        const val MAX_ROUNDNESS = 1f
         private const val DARK_THEME_STROKE_ALPHA = 51
         private const val LIGHT_THEME_STROKE_ALPHA = 41
         private const val DARK_THEME_SHADOW_ALPHA = 51f
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index d94d9175..34ab9f0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
+import com.android.launcher3.util.DisplayController;
 import com.android.systemui.shared.rotation.RotationButtonController;
 
 import java.io.PrintWriter;
@@ -64,6 +65,7 @@
     public final KeyboardQuickSwitchController keyboardQuickSwitchController;
     public final TaskbarPinningController taskbarPinningController;
     public final Optional<BubbleControllers> bubbleControllers;
+    public final TaskbarDesktopModeController taskbarDesktopModeController;
 
     @Nullable private LoggableTaskbarController[] mControllersToLog = null;
     @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
@@ -111,7 +113,8 @@
             TaskbarEduTooltipController taskbarEduTooltipController,
             KeyboardQuickSwitchController keyboardQuickSwitchController,
             TaskbarPinningController taskbarPinningController,
-            Optional<BubbleControllers> bubbleControllers) {
+            Optional<BubbleControllers> bubbleControllers,
+            TaskbarDesktopModeController taskbarDesktopModeController) {
         this.taskbarActivityContext = taskbarActivityContext;
         this.taskbarDragController = taskbarDragController;
         this.navButtonController = navButtonController;
@@ -138,6 +141,7 @@
         this.keyboardQuickSwitchController = keyboardQuickSwitchController;
         this.taskbarPinningController = taskbarPinningController;
         this.bubbleControllers = bubbleControllers;
+        this.taskbarDesktopModeController = taskbarDesktopModeController;
     }
 
     /**
@@ -173,6 +177,7 @@
         taskbarEduTooltipController.init(this);
         keyboardQuickSwitchController.init(this);
         taskbarPinningController.init(this, mSharedState);
+        taskbarDesktopModeController.init(this, mSharedState);
 
         mControllersToLog = new LoggableTaskbarController[] {
                 taskbarDragController, navButtonController, navbarButtonsViewController,
@@ -188,7 +193,13 @@
                 taskbarDragLayerController, taskbarScrimViewController,
                 voiceInteractionWindowController
         };
-        mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS);
+
+        if (DisplayController.isInDesktopMode(taskbarActivityContext)) {
+            mCornerRoundness.updateValue(taskbarDesktopModeController.getTaskbarCornerRoundness(
+                    mSharedState.showCornerRadiusInDesktopMode));
+        } else {
+            mCornerRoundness.updateValue(TaskbarBackgroundRenderer.MAX_ROUNDNESS);
+        }
 
         mAreAllControllersInitialized = true;
         for (Runnable postInitCallback : mPostInitCallbacks) {
@@ -248,6 +259,7 @@
         keyboardQuickSwitchController.onDestroy();
         taskbarStashController.onDestroy();
         bubbleControllers.ifPresent(controllers -> controllers.onDestroy());
+        taskbarDesktopModeController.onDestroy();
 
         mControllersToLog = null;
         mBackgroundRendererControllers = null;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
new file mode 100644
index 0000000..a376531
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statehandlers.DesktopVisibilityController.TaskbarDesktopModeListener
+import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS
+
+/** Handles Taskbar in Desktop Windowing mode. */
+class TaskbarDesktopModeController(
+    private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?
+) : TaskbarDesktopModeListener {
+    private lateinit var taskbarControllers: TaskbarControllers
+    private lateinit var taskbarSharedState: TaskbarSharedState
+
+    private val desktopVisibilityController: DesktopVisibilityController?
+        get() = desktopVisibilityControllerProvider()
+
+    fun init(controllers: TaskbarControllers, sharedState: TaskbarSharedState) {
+        taskbarControllers = controllers
+        taskbarSharedState = sharedState
+        desktopVisibilityController?.registerTaskbarDesktopModeListener(this)
+    }
+
+    override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {
+        taskbarSharedState.showCornerRadiusInDesktopMode = doesAnyTaskRequireTaskbarRounding
+        val cornerRadius = getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding)
+        taskbarControllers.taskbarCornerRoundness.animateToValue(cornerRadius).start()
+    }
+
+    fun getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding: Boolean): Float {
+        return if (doesAnyTaskRequireTaskbarRounding) {
+            MAX_ROUNDNESS
+        } else {
+            0f
+        }
+    }
+
+    fun onDestroy() = desktopVisibilityController?.unregisterTaskbarDesktopModeListener(this)
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 0eb8890..63fae8c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -48,6 +48,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
@@ -591,6 +592,12 @@
 
         float cornerRoundness = isInLauncher ? 0 : 1;
 
+        if (DisplayController.isInDesktopMode(mLauncher) && mControllers.getSharedState() != null) {
+            cornerRoundness =
+                    mControllers.taskbarDesktopModeController.getTaskbarCornerRoundness(
+                            mControllers.getSharedState().showCornerRadiusInDesktopMode);
+        }
+
         // Don't animate if corner roundness has reached desired value.
         if (mTaskbarCornerRoundness.isAnimating()
                 || mTaskbarCornerRoundness.value != cornerRoundness) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
index 77bd35f..729cbe9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
@@ -102,5 +102,8 @@
 
     // To track if taskbar was stashed / unstashed between configuration changes (which recreates
     // the task bar).
-    public Boolean taskbarWasStashedAuto = true;
+    public boolean taskbarWasStashedAuto = true;
+
+    // should show corner radius on persistent taskbar when in desktop mode.
+    public boolean showCornerRadiusInDesktopMode = false;
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index aef21aa..27c1d9c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -219,8 +219,8 @@
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
             // This gets modified in NavbarButtonsViewController, but the initial value it reads
             // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here
-            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN)
-                    .animateToValue(mActivity.isPhoneButtonNavMode() ? 0 : 1).start();
+            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN).setValue(
+                    mActivity.isPhoneMode() ? 0 : 1);
         }
         if (enableTaskbarPinning()) {
             mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener);
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 84f6b55..a03c0f8 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.window.CachedDisplayInfo;
+import com.android.systemui.shared.Flags;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -242,7 +243,8 @@
         int rotation = display.rotation;
         int touchHeight = mNavBarGesturalHeight;
         OrientationRectF orientationRectF = new OrientationRectF(0, 0, size.x, size.y, rotation);
-        if (mMode == NavigationMode.NO_BUTTON) {
+        if (mMode == NavigationMode.NO_BUTTON
+                || (mMode == NavigationMode.THREE_BUTTONS && Flags.threeButtonCornerSwipe())) {
             orientationRectF.top = orientationRectF.bottom - touchHeight;
             updateAssistantRegions(orientationRectF);
         } else {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index f902284..5131774 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -74,6 +74,7 @@
 import com.android.quickstep.util.GestureExclusionManager;
 import com.android.quickstep.util.GestureExclusionManager.ExclusionListener;
 import com.android.quickstep.util.NavBarPosition;
+import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -548,6 +549,13 @@
     }
 
     /**
+     * @return whether the Assistant gesture can be used in 3 button navigation mode.
+     */
+    public boolean supportsAssistantGestureInButtonNav() {
+        return Flags.threeButtonCornerSwipe();
+    }
+
+    /**
      * @param ev An ACTION_DOWN motion event
      * @return whether the given motion event can trigger the assistant over the current task.
      */
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index db03dac..a01ceb2 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.recents.data.RecentTasksDataSource;
+import com.android.quickstep.recents.data.TaskVisualsChangeNotifier;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TaskVisualsChangeListener;
@@ -65,7 +66,8 @@
  */
 @TargetApi(Build.VERSION_CODES.O)
 public class RecentsModel implements RecentTasksDataSource, IconChangeListener,
-        TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable {
+        TaskStackChangeListener, TaskVisualsChangeListener, TaskVisualsChangeNotifier,
+        SafeCloseable {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
@@ -287,6 +289,7 @@
     /**
      * Adds a listener for visuals changes
      */
+    @Override
     public void addThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.add(listener);
     }
@@ -294,6 +297,7 @@
     /**
      * Removes a previously added listener
      */
+    @Override
     public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.remove(listener);
     }
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 6f1ab7d..80c07196 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -157,7 +158,7 @@
         // Register for navigation mode changes
         mDisplayController.addChangeListener(this);
         DisplayController.Info info = mDisplayController.getInfo();
-        onDisplayInfoChangedInternal(info, CHANGE_ALL, info.getNavigationMode().hasGestures);
+        onDisplayInfoChangedInternal(info, CHANGE_ALL, hasGestures(info.getNavigationMode()));
         runOnDestroy(() -> mDisplayController.removeChangeListener(this));
 
         mOrientationListener = new OrientationEventListener(mContext) {
@@ -229,7 +230,7 @@
      * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
      */
     public void updateGestureTouchRegions() {
-        if (!mMode.hasGestures) {
+        if (!hasGestures(mMode)) {
             return;
         }
 
@@ -268,7 +269,7 @@
                 | CHANGE_SUPPORTED_BOUNDS)) != 0) {
             mDisplayRotation = info.rotation;
 
-            if (mMode.hasGestures) {
+            if (hasGestures(mMode)) {
                 updateGestureTouchRegions();
                 mOrientationTouchTransformer.createOrAddTouchRegion(info);
                 mCurrentAppRotation = mDisplayRotation;
@@ -295,9 +296,9 @@
             mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
                     mContext.getResources());
 
-            if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) {
+            if (forceRegister || (!hasGestures(mMode) && hasGestures(newMode))) {
                 setupOrientationSwipeHandler();
-            } else if (mMode.hasGestures && !newMode.hasGestures) {
+            } else if (hasGestures(mMode) && !hasGestures(newMode)) {
                 destroyOrientationSwipeHandlerCallback();
             }
 
@@ -399,7 +400,7 @@
     }
 
     public int getCurrentActiveRotation() {
-        if (!mMode.hasGestures) {
+        if (!hasGestures(mMode)) {
             // touch rotation should always match that of display for 3 button
             return mDisplayRotation;
         }
@@ -416,4 +417,8 @@
     public OrientationTouchTransformer getOrientationTouchTransformer() {
         return mOrientationTouchTransformer;
     }
+
+    private boolean hasGestures(NavigationMode mode) {
+        return mode.hasGestures || (mode == THREE_BUTTONS && Flags.threeButtonCornerSwipe());
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 3c6c3e4..580dcc2 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -27,6 +27,7 @@
 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;
@@ -48,7 +49,7 @@
     private final boolean mEnableTaskSnapshotPreloading;
     private final Context mContext;
 
-    public static class HighResLoadingState {
+    public static class HighResLoadingState implements HighResLoadingStateNotifier {
         private boolean mForceHighResThumbnails;
         private boolean mVisible;
         private boolean mFlingingFast;
@@ -65,11 +66,13 @@
             mForceHighResThumbnails = !supportsLowResThumbnails();
         }
 
-        public void addCallback(HighResLoadingStateChangedCallback callback) {
+        @Override
+        public void addCallback(@NonNull HighResLoadingStateChangedCallback callback) {
             mCallbacks.add(callback);
         }
 
-        public void removeCallback(HighResLoadingStateChangedCallback callback) {
+        @Override
+        public void removeCallback(@NonNull HighResLoadingStateChangedCallback callback) {
             mCallbacks.remove(callback);
         }
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2b5aa71..b321b3e 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -89,6 +89,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ConstantItem;
 import com.android.launcher3.EncryptionType;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.config.FeatureFlags;
@@ -674,8 +675,9 @@
     private void initInputMonitor(String reason) {
         disposeEventHandlers("Initializing input monitor due to: " + reason);
 
-        if (mDeviceState.isButtonNavMode() && (!ENABLE_TRACKPAD_GESTURE.get()
-                || mTrackpadsConnected.isEmpty())) {
+        if (mDeviceState.isButtonNavMode()
+                && !mDeviceState.supportsAssistantGestureInButtonNav()
+                && (!ENABLE_TRACKPAD_GESTURE.get() || mTrackpadsConnected.isEmpty())) {
             return;
         }
 
@@ -857,7 +859,9 @@
                             .append("); cancelling gesture."),
                     NAVIGATION_MODE_SWITCHED);
             event.setAction(ACTION_CANCEL);
-        } else if (mDeviceState.isButtonNavMode() && !isTrackpadMotionEvent(event)) {
+        } else if (mDeviceState.isButtonNavMode()
+                && !mDeviceState.supportsAssistantGestureInButtonNav()
+                && !isTrackpadMotionEvent(event)) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
                     .append("Cannot process input event: ")
                     .append("using 3-button nav and event is not a trackpad event"));
@@ -909,7 +913,22 @@
             if (isInSwipeUpTouchRegion && tac != null) {
                 tac.closeKeyboardQuickSwitchView();
             }
-            if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
+            if (mDeviceState.isButtonNavMode()
+                    && mDeviceState.supportsAssistantGestureInButtonNav()) {
+                reasonString.append("in three button mode which supports Assistant gesture");
+                // Consume gesture event for Assistant (all other gestures should do nothing).
+                if (mDeviceState.canTriggerAssistantAction(event)) {
+                    reasonString.append(" and event can trigger assistant action")
+                            .append(", consuming gesture for assistant action");
+                    mGestureState =
+                            createGestureState(mGestureState, getTrackpadGestureType(event));
+                    mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
+                } else {
+                    reasonString.append(" but event cannot trigger Assistant")
+                            .append(", consuming gesture as no-op");
+                    mUncheckedConsumer = InputConsumer.NO_OP;
+                }
+            } else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
                     || isHoverActionWithoutConsumer || isOnBubbles) {
                 reasonString.append(!isOneHandedModeActive && isInSwipeUpTouchRegion
                                 ? "one handed mode is not active and event is in swipe up region"
@@ -931,8 +950,7 @@
                                 : "event is a trackpad multi-finger swipe")
                         .append(" and event can trigger assistant action")
                         .append(", consuming gesture for assistant action");
-                mGestureState = createGestureState(mGestureState,
-                        getTrackpadGestureType(event));
+                mGestureState = createGestureState(mGestureState, getTrackpadGestureType(event));
                 // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
                 // should not interrupt it. QuickSwitch assumes that interruption can only
                 // happen if the next gesture is also quick switch.
@@ -1193,7 +1211,8 @@
             NavHandle navHandle = tac != null ? tac.getNavHandle()
                     : SystemUiProxy.INSTANCE.get(this);
             if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
-                    && navHandle.canNavHandleBeLongPressed()) {
+                    && navHandle.canNavHandleBeLongPressed()
+                    && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
                 reasonString.append(NEWLINE_PREFIX)
                         .append(reasonPrefix)
                         .append(SUBSTRING_PREFIX)
@@ -1289,6 +1308,11 @@
         return new CompoundString(NEWLINE_PREFIX).append(substring);
     }
 
+    private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
+        return Flags.ignoreThreeFingerTrackpadForNavHandleLongPress()
+                && gestureState.isThreeFingerTrackpadGesture();
+    }
+
     private void logInputConsumerSelectionReason(
             InputConsumer consumer, CompoundString reasonString) {
         ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ")
diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
new file mode 100644
index 0000000..df546ca
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+
+/** Notifies added callbacks that high res state has changed */
+interface HighResLoadingStateNotifier {
+    /** Adds a callback for high res loading state */
+    fun addCallback(callback: HighResLoadingStateChangedCallback)
+
+    /** Removes a callback for high res loading state */
+    fun removeCallback(callback: HighResLoadingStateChangedCallback)
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index 4f7a541..9c4248c 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -41,10 +41,4 @@
      * populated e.g. icons/thumbnails etc.
      */
     fun setVisibleTasks(visibleTaskIdList: List<Int>)
-
-    /**
-     * Override [ThumbnailData] with a map of taskId to [ThumbnailData]. The override only applies
-     * if the tasks are already visible, and will be invalidated when tasks become invisible.
-     */
-    fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>)
 }
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt
new file mode 100644
index 0000000..6e7789d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.TaskVisualsChangeListener
+
+/** Notifies added listeners that task visuals have changed */
+interface TaskVisualsChangeNotifier {
+    /** Adds a listener for visuals changes */
+    fun addThumbnailChangeListener(listener: TaskVisualsChangeListener)
+
+    /** Removes a listener for visuals changes */
+    fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener)
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
new file mode 100644
index 0000000..a141e89
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.os.UserHandle
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
+import com.android.quickstep.util.TaskVisualsChangeListener
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+
+/** Delegates the checking of task visuals (thumbnails, high res changes, icons) */
+interface TaskVisualsChangedDelegate :
+    TaskVisualsChangeListener, HighResLoadingStateChangedCallback {
+    /** Registers a callback for visuals relating to icons */
+    fun registerTaskIconChangedCallback(
+        taskKey: Task.TaskKey,
+        taskIconChangedCallback: TaskIconChangedCallback
+    )
+
+    /** Unregisters a callback for visuals relating to icons */
+    fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey)
+
+    /** Registers a callback for visuals relating to thumbnails */
+    fun registerTaskThumbnailChangedCallback(
+        taskKey: Task.TaskKey,
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback
+    )
+
+    /** Unregisters a callback for visuals relating to thumbnails */
+    fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey)
+
+    /** A callback for task icon changes */
+    interface TaskIconChangedCallback {
+        /** Informs the listener that the task icon has changed */
+        fun onTaskIconChanged()
+    }
+
+    /** A callback for task thumbnail changes */
+    interface TaskThumbnailChangedCallback {
+        /** Informs the listener that the task thumbnail data has changed to [thumbnailData] */
+        fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?)
+
+        /** Informs the listener that the default resolution for loading thumbnails has changed */
+        fun onHighResLoadingStateChanged()
+    }
+}
+
+class TaskVisualsChangedDelegateImpl(
+    private val taskVisualsChangeNotifier: TaskVisualsChangeNotifier,
+    private val highResLoadingStateNotifier: HighResLoadingStateNotifier,
+) : TaskVisualsChangedDelegate {
+    private val taskIconChangedCallbacks =
+        mutableMapOf<Int, Pair<Task.TaskKey, TaskIconChangedCallback>>()
+    private val taskThumbnailChangedCallbacks =
+        mutableMapOf<Int, Pair<Task.TaskKey, TaskThumbnailChangedCallback>>()
+    private var isListening = false
+
+    @Synchronized
+    private fun onCallbackRegistered() {
+        if (isListening) return
+
+        taskVisualsChangeNotifier.addThumbnailChangeListener(this)
+        highResLoadingStateNotifier.addCallback(this)
+        isListening = true
+    }
+
+    @Synchronized
+    private fun onCallbackUnregistered() {
+        if (!isListening) return
+
+        if (taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size == 0) {
+            taskVisualsChangeNotifier.removeThumbnailChangeListener(this)
+            highResLoadingStateNotifier.removeCallback(this)
+        }
+
+        isListening = false
+    }
+
+    override fun onTaskIconChanged(taskId: Int) {
+        taskIconChangedCallbacks[taskId]?.let { (_, callback) -> callback.onTaskIconChanged() }
+    }
+
+    override fun onTaskIconChanged(pkg: String, user: UserHandle) {
+        taskIconChangedCallbacks.values
+            .filter { (taskKey, _) ->
+                pkg == taskKey.packageName && user.identifier == taskKey.userId
+            }
+            .forEach { (_, callback) -> callback.onTaskIconChanged() }
+    }
+
+    override fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData?): Task? {
+        taskThumbnailChangedCallbacks[taskId]?.let { (_, callback) ->
+            callback.onTaskThumbnailChanged(thumbnailData)
+        }
+        return null
+    }
+
+    override fun onHighResLoadingStateChanged(enabled: Boolean) {
+        taskThumbnailChangedCallbacks.values.forEach { (_, callback) ->
+            callback.onHighResLoadingStateChanged()
+        }
+    }
+
+    override fun registerTaskIconChangedCallback(
+        taskKey: Task.TaskKey,
+        taskIconChangedCallback: TaskIconChangedCallback
+    ) {
+        taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback
+        onCallbackRegistered()
+    }
+
+    override fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) {
+        taskIconChangedCallbacks.remove(taskKey.id)
+        onCallbackUnregistered()
+    }
+
+    override fun registerTaskThumbnailChangedCallback(
+        taskKey: Task.TaskKey,
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback
+    ) {
+        taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback
+        onCallbackRegistered()
+    }
+
+    override fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) {
+        taskThumbnailChangedCallbacks.remove(taskKey.id)
+        onCallbackUnregistered()
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 6f9d157..eb3c2d1 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -18,6 +18,8 @@
 
 import android.graphics.drawable.Drawable
 import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
 import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
 import com.android.quickstep.util.GroupTask
@@ -26,18 +28,20 @@
 import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
@@ -46,12 +50,12 @@
     private val recentsModel: RecentTasksDataSource,
     private val taskThumbnailDataSource: TaskThumbnailDataSource,
     private val taskIconDataSource: TaskIconDataSource,
+    private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate,
     recentsCoroutineScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
 ) : RecentTasksRepository {
     private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
     private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
-    private val thumbnailOverride = MutableStateFlow(mapOf<Int, ThumbnailData>())
 
     private val taskData =
         groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
@@ -85,15 +89,13 @@
             .distinctUntilChanged()
 
     private val augmentedTaskData: Flow<List<Task>> =
-        combine(taskData, thumbnailQueryResults, iconQueryResults, thumbnailOverride) {
+        combine(taskData, thumbnailQueryResults, iconQueryResults) {
                 tasks,
                 thumbnailQueryResults,
-                iconQueryResults,
-                thumbnailOverride ->
+                iconQueryResults ->
                 tasks.onEach { task ->
                     // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible)
-                    task.thumbnail =
-                        thumbnailOverride[task.key.id] ?: thumbnailQueryResults[task.key.id]
+                    task.thumbnail = thumbnailQueryResults[task.key.id]
 
                     // TODO(b/352331675) don't load icons for DesktopTaskView
                     // Add retrieved icons + remove unnecessary icons
@@ -121,61 +123,76 @@
 
     override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         this.visibleTaskIds.value = visibleTaskIdList.toSet()
-        addOrUpdateThumbnailOverride(emptyMap())
-    }
-
-    override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
-        this.thumbnailOverride.value =
-            this.thumbnailOverride.value
-                .toMutableMap()
-                .apply { putAll(thumbnailOverride) }
-                .filterKeys(this.visibleTaskIds.value::contains)
     }
 
     /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
-    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = flow {
-        emit(task.key.id to task.thumbnail)
-        val thumbnailDataResult: ThumbnailData? =
-            withContext(dispatcherProvider.main) {
-                suspendCancellableCoroutine { continuation ->
-                    val cancellableTask =
-                        taskThumbnailDataSource.getThumbnailInBackground(task) {
-                            continuation.resume(it)
-                        }
-                    continuation.invokeOnCancellation { cancellableTask?.cancel() }
+    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow {
+        trySend(task.key.id to task.thumbnail)
+        trySend(task.key.id to getThumbnailFromDataSource(task))
+
+        val callback =
+            object : TaskThumbnailChangedCallback {
+                override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) {
+                    trySend(task.key.id to thumbnailData)
+                }
+
+                override fun onHighResLoadingStateChanged() {
+                    launch { trySend(task.key.id to getThumbnailFromDataSource(task)) }
                 }
             }
-        emit(task.key.id to thumbnailDataResult)
+        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback)
+        awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) }
     }
 
-    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
+    /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */
     private fun getIconDataRequest(task: Task): IconDataRequest =
-        flow {
-                emit(task.key.id to task.getTaskIconQueryResponse())
-                val iconDataResponse: TaskIconQueryResponse? =
-                    withContext(dispatcherProvider.main) {
-                        suspendCancellableCoroutine { continuation ->
-                            val cancellableTask =
-                                taskIconDataSource.getIconInBackground(task) {
-                                    icon,
-                                    contentDescription,
-                                    title ->
-                                    icon.constantState?.let {
-                                        continuation.resume(
-                                            TaskIconQueryResponse(
-                                                it.newDrawable().mutate(),
-                                                contentDescription,
-                                                title
-                                            )
-                                        )
-                                    }
-                                }
-                            continuation.invokeOnCancellation { cancellableTask?.cancel() }
+        callbackFlow {
+                trySend(task.key.id to task.getTaskIconQueryResponse())
+                trySend(task.key.id to getIconFromDataSource(task))
+
+                val callback =
+                    object : TaskIconChangedCallback {
+                        override fun onTaskIconChanged() {
+                            launch { trySend(task.key.id to getIconFromDataSource(task)) }
                         }
                     }
-                emit(task.key.id to iconDataResponse)
+                taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback)
+                awaitClose {
+                    taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key)
+                }
             }
             .distinctUntilChanged()
+
+    private suspend fun getThumbnailFromDataSource(task: Task) =
+        withContext(dispatcherProvider.main) {
+            suspendCancellableCoroutine { continuation ->
+                val cancellableTask =
+                    taskThumbnailDataSource.getThumbnailInBackground(task) {
+                        continuation.resume(it)
+                    }
+                continuation.invokeOnCancellation { cancellableTask?.cancel() }
+            }
+        }
+
+    private suspend fun getIconFromDataSource(task: Task) =
+        withContext(dispatcherProvider.main) {
+            suspendCancellableCoroutine { continuation ->
+                val cancellableTask =
+                    taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title
+                        ->
+                        icon.constantState?.let {
+                            continuation.resume(
+                                TaskIconQueryResponse(
+                                    it.newDrawable().mutate(),
+                                    contentDescription,
+                                    title
+                                )
+                            )
+                        }
+                    }
+                continuation.invokeOnCancellation { cancellableTask?.cancel() }
+            }
+        }
 }
 
 data class TaskIconQueryResponse(
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 0b672d1..0a5544f 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -22,6 +22,8 @@
 import com.android.launcher3.util.coroutines.ProductionDispatchers
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl
 import com.android.quickstep.recents.data.TasksRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.GetThumbnailUseCase
@@ -60,14 +62,22 @@
             val recentsCoroutineScope =
                 CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView"))
             set(CoroutineScope::class.java.simpleName, recentsCoroutineScope)
+            val recentsModel = RecentsModel.INSTANCE.get(appContext)
+            val taskVisualsChangedDelegate =
+                TaskVisualsChangedDelegateImpl(
+                    recentsModel,
+                    recentsModel.thumbnailCache.highResLoadingState
+                )
+            set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate)
 
             // Create RecentsTaskRepository singleton
             val recentTasksRepository: RecentTasksRepository =
-                with(RecentsModel.INSTANCE.get(appContext)) {
+                with(recentsModel) {
                     TasksRepository(
                         this,
                         thumbnailCache,
                         iconCache,
+                        taskVisualsChangedDelegate,
                         recentsCoroutineScope,
                         ProductionDispatchers
                     )
@@ -155,6 +165,7 @@
                             thumbnailCache,
                             iconCache,
                             get(),
+                            get(),
                             ProductionDispatchers
                         )
                     }
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index b1f46a3..1716f2e 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -58,10 +58,6 @@
         recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
     }
 
-    fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
-        recentsTasksRepository.addOrUpdateThumbnailOverride(thumbnailOverride)
-    }
-
     suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>) {
         combine(
                 updatedThumbnails.map {
diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
index 66bff73..519ef60 100644
--- a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.util;
 
+import android.annotation.NonNull;
 import android.os.UserHandle;
 
 import com.android.systemui.shared.recents.model.Task;
@@ -36,7 +37,7 @@
     /**
      * Called when the icon for a task changes
      */
-    default void onTaskIconChanged(String pkg, UserHandle user) {}
+    default void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {}
 
     /**
      * Called when the icon for a task changes
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 69a9690..255619a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1079,7 +1079,6 @@
     @Nullable
     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
         if (enableRefactorTaskThumbnail()) {
-            mHelper.onTaskThumbnailChanged(taskId, thumbnailData);
             return null;
         }
         if (mHandleTaskStackChanges) {
@@ -1100,8 +1099,7 @@
     }
 
     @Override
-    public void onTaskIconChanged(String pkg, UserHandle user) {
-        // TODO(b/342560598): Listen in TaskRepository and reload.
+    public void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView tv = requireTaskViewAt(i);
             Task task = tv.getFirstTask();
@@ -2549,7 +2547,6 @@
         }
 
         if (enableRefactorTaskThumbnail()) {
-            // TODO(b/342560598): Listen in TaskRepository and reload.
             return;
         }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index 5b71da1..f5b2176 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -68,8 +68,4 @@
             ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
         }
     }
-
-    fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData) {
-        recentsViewModel.addOrUpdateThumbnailOverride(mapOf(taskId to thumbnailData))
-    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt
new file mode 100644
index 0000000..72bbfc9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+class TaskbarDesktopModeControllerTest {
+
+    private val context =
+        TaskbarWindowSandboxContext.create(
+            InstrumentationRegistry.getInstrumentation().targetContext
+        )
+
+    @get:Rule(order = 0) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @TaskbarUnitTestRule.InjectController
+    lateinit var taskbarDesktopModeController: TaskbarDesktopModeController
+
+    @Test
+    fun whenTaskbarRequiresCornerRoundness_shouldReturnDefaultCornerRoundness() {
+        assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(true))
+            .isEqualTo(MAX_ROUNDNESS)
+    }
+
+    @Test
+    fun whenTaskbarRequiresCornerRoundness_shouldReturnZeroAsCornerRoundness() {
+        assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(false)).isEqualTo(0f)
+    }
+}
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
new file mode 100644
index 0000000..7d09efd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+
+class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier {
+    val listeners = mutableListOf<HighResLoadingStateChangedCallback>()
+
+    override fun addCallback(callback: HighResLoadingStateChangedCallback) {
+        listeners.add(callback)
+    }
+
+    override fun removeCallback(callback: HighResLoadingStateChangedCallback) {
+        listeners.remove(callback)
+    }
+}
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 fee4979..5de876a 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
@@ -27,7 +27,8 @@
 
 class FakeTaskIconDataSource : TaskIconDataSource {
 
-    val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mockCopyableDrawable() }
+    val taskIdToDrawable: MutableMap<Int, Drawable> =
+        (0..10).associateWith { mockCopyableDrawable() }.toMutableMap()
 
     val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
     var shouldLoadSynchronously: Boolean = true
@@ -52,15 +53,17 @@
         return null
     }
 
-    private fun mockCopyableDrawable(): Drawable {
-        val mutableDrawable = mock<Drawable>()
-        val immutableDrawable =
-            mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
-        val constantState =
-            mock<Drawable.ConstantState>().apply {
-                whenever(newDrawable()).thenReturn(immutableDrawable)
-            }
-        return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
+    companion object {
+        fun mockCopyableDrawable(): Drawable {
+            val mutableDrawable = mock<Drawable>()
+            val immutableDrawable =
+                mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
+            val constantState =
+                mock<Drawable.ConstantState>().apply {
+                    whenever(newDrawable()).thenReturn(immutableDrawable)
+                }
+            return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
+        }
     }
 }
 
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 30fc491..d12c0b0 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
@@ -27,7 +27,8 @@
 
 class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
 
-    val taskIdToBitmap: Map<Int, Bitmap> = (0..10).associateWith { mock() }
+    val taskIdToBitmap: MutableMap<Int, Bitmap> =
+        (0..10).associateWith { mock<Bitmap>() }.toMutableMap()
     val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
     var shouldLoadSynchronously: Boolean = true
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt
new file mode 100644
index 0000000..765f0d1
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.TaskVisualsChangeListener
+
+class FakeTaskVisualsChangeNotifier : TaskVisualsChangeNotifier {
+    val listeners = mutableListOf<TaskVisualsChangeListener>()
+
+    override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) {
+        listeners.remove(listener)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index d94a351..7a17872 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -28,7 +28,6 @@
     private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
     private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
     private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
-    private var thumbnailOverrideMap: Map<Int, ThumbnailData> = emptyMap()
 
     override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks
 
@@ -39,7 +38,7 @@
             .map { taskList ->
                 val task = taskList.firstOrNull { it.key.id == taskId } ?: return@map null
                 Task(task).apply {
-                    thumbnail = thumbnailOverrideMap[taskId] ?: task.thumbnail
+                    thumbnail = task.thumbnail
                     icon = task.icon
                     titleDescription = task.titleDescription
                     title = task.title
@@ -62,16 +61,6 @@
                     }
                 }
             }
-        setThumbnailOverrideInternal(thumbnailOverrideMap)
-    }
-
-    override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
-        setThumbnailOverrideInternal(thumbnailOverride)
-    }
-
-    private fun setThumbnailOverrideInternal(thumbnailOverride: Map<Int, ThumbnailData>) {
-        thumbnailOverrideMap =
-            thumbnailOverride.filterKeys(this.visibleTasks.value::contains).toMap()
     }
 
     fun seedTasks(tasks: List<Task>) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
new file mode 100644
index 0000000..41f6bfd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.UserHandle
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+class TaskVisualsChangedDelegateTest {
+    private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
+    private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
+
+    val systemUnderTest =
+        TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)
+
+    @Test
+    fun addingFirstListener_addsListenerToNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+
+        assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest)
+        assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest)
+    }
+
+    @Test
+    fun addingAndRemovingListener_removesListenerFromNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+        systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1))
+
+        assertThat(taskVisualsChangeNotifier.listeners).isEmpty()
+        assertThat(highResLoadingStateNotifier.listeners).isEmpty()
+    }
+
+    @Test
+    fun addingTwoAndRemovingOneListener_doesNotRemoveListenerFromNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 2), mock())
+        systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1))
+
+        assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest)
+        assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest)
+    }
+
+    @Test
+    fun onTaskIconChangedWithTaskId_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskIconChangedCallback>()
+        val additionalListener = mock<TaskIconChangedCallback>()
+        systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 1), expectedListener)
+        systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 2), additionalListener)
+
+        systemUnderTest.onTaskIconChanged(1)
+
+        verify(expectedListener).onTaskIconChanged()
+        verifyNoMoreInteractions(additionalListener)
+    }
+
+    @Test
+    fun onTaskIconChangedWithoutTaskId_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskIconChangedCallback>()
+        val listener = mock<TaskIconChangedCallback>()
+        // Correct match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            expectedListener
+        )
+        // 1 out of 2 match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1),
+            listener
+        )
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2),
+            listener
+        )
+        // 0 out of 2 match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2),
+            listener
+        )
+
+        systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
+
+        verify(expectedListener).onTaskIconChanged()
+        verifyNoMoreInteractions(listener)
+    }
+
+    @Test
+    fun replacedTaskIconChangedCallbacks_notCalled() {
+        val replacedListener = mock<TaskIconChangedCallback>()
+        val newListener = mock<TaskIconChangedCallback>()
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            replacedListener
+        )
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            newListener
+        )
+
+        systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
+
+        verifyNoMoreInteractions(replacedListener)
+        verify(newListener).onTaskIconChanged()
+    }
+
+    @Test
+    fun onTaskThumbnailChanged_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskThumbnailChangedCallback>()
+        val additionalListener = mock<TaskThumbnailChangedCallback>()
+        val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            expectedListener
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 2),
+            additionalListener
+        )
+
+        systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+        verify(expectedListener).onTaskThumbnailChanged(expectedThumbnailData)
+        verifyNoMoreInteractions(additionalListener)
+    }
+
+    @Test
+    fun onHighResLoadingStateChanged_notifiesAllListeners() {
+        val expectedListener = mock<TaskThumbnailChangedCallback>()
+        val additionalListener = mock<TaskThumbnailChangedCallback>()
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            expectedListener
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 2),
+            additionalListener
+        )
+
+        systemUnderTest.onHighResLoadingStateChanged(true)
+
+        verify(expectedListener).onHighResLoadingStateChanged()
+        verify(additionalListener).onHighResLoadingStateChanged()
+    }
+
+    @Test
+    fun replacedTaskThumbnailChangedCallbacks_notCalled() {
+        val replacedListener1 = mock<TaskThumbnailChangedCallback>()
+        val newListener1 = mock<TaskThumbnailChangedCallback>()
+        val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            replacedListener1
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1)
+
+        systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+        verifyNoMoreInteractions(replacedListener1)
+        verify(newListener1).onTaskThumbnailChanged(expectedThumbnailData)
+    }
+
+    private fun createTaskKey(id: Int = 1, pkg: String = PACKAGE_NAME, userId: Int = 1) =
+        TaskKey(id, 0, Intent().setPackage(pkg), ComponentName("", ""), userId, 0)
+
+    private companion object {
+        const val PACKAGE_NAME = "com.test.test"
+        const val ALTERNATIVE_PACKAGE_NAME = "com.test.test2"
+    }
+}
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 e6534eb..f31467f 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
@@ -19,7 +19,7 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.graphics.Bitmap
-import android.view.Surface
+import android.graphics.drawable.Drawable
 import com.android.launcher3.util.TestDispatcherProvider
 import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest
 import com.android.quickstep.util.DesktopTask
@@ -29,6 +29,9 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
@@ -48,6 +51,10 @@
     private val recentsModel = FakeRecentTasksDataSource()
     private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
     private val taskIconDataSource = FakeTaskIconDataSource()
+    private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
+    private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
+    private val taskVisualsChangedDelegate =
+        TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)
 
     private val dispatcher = UnconfinedTestDispatcher()
     private val testScope = TestScope(dispatcher)
@@ -56,6 +63,7 @@
             recentsModel,
             taskThumbnailDataSource,
             taskIconDataSource,
+            taskVisualsChangedDelegate,
             testScope.backgroundScope,
             TestDispatcherProvider(dispatcher)
         )
@@ -203,87 +211,81 @@
         }
 
     @Test
-    fun addThumbnailOverrideOverrideThumbnails() =
+    fun onTaskThumbnailChanged_setsNewThumbnailDataOnTask() =
         testScope.runTest {
             recentsModel.seedTasks(defaultTaskList)
-            val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-            val thumbnailOverride2 = createThumbnailData()
-            systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
-
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride2.thumbnail)
-        }
-
-    @Test
-    fun addThumbnailOverrideMultipleOverrides() =
-        testScope.runTest {
-            recentsModel.seedTasks(defaultTaskList)
-            val thumbnailOverride1 = createThumbnailData()
-            val thumbnailOverride2 = createThumbnailData()
-            val thumbnailOverride3 = createThumbnailData()
-            systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride3))
-
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride1.thumbnail)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride3.thumbnail)
-        }
-
-    @Test
-    fun addThumbnailOverrideClearedWhenTaskBecomeInvisible() =
-        testScope.runTest {
-            recentsModel.seedTasks(defaultTaskList)
-            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-            val thumbnailOverride1 = createThumbnailData()
-            val thumbnailOverride2 = createThumbnailData()
-            systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
-            // Making task 2 invisible and visible again should clear the override
-            systemUnderTest.setVisibleTasks(listOf(1))
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride1.thumbnail)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2)
-        }
-
-    @Test
-    fun addThumbnailOverrideDoesNotOverrideInvisibleTasks() =
-        testScope.runTest {
-            recentsModel.seedTasks(defaultTaskList)
-            val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-            val thumbnailOverride = createThumbnailData()
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
             systemUnderTest.setVisibleTasks(listOf(1))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride))
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
 
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2)
+            val expectedThumbnailData = createThumbnailData()
+            val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
+            }
+            taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+            assertThat(task1ThumbnailValues[1]!!.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData)
+        }
+
+    @Test
+    fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(listOf(1))
+
+            val expectedBitmap = mock<Bitmap>()
+            val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1ThumbnailValues = mutableListOf<Bitmap?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
+            }
+            taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+            taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
+
+            assertThat(task1ThumbnailValues[1]).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
+        }
+
+    @Test
+    fun onTaskIconChanged_setsNewIconOnTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(listOf(1))
+
+            val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable()
+            val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1IconValues = mutableListOf<Drawable?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.icon }.toList(task1IconValues)
+            }
+            taskIconDataSource.taskIdToDrawable[1] = expectedIcon
+            taskVisualsChangedDelegate.onTaskIconChanged(1)
+
+            assertThat(task1IconValues[1]).isEqualTo(expectedPreviousIcon)
+            assertThat(task1IconValues.last()).isEqualTo(expectedIcon)
         }
 
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000))
 
-    private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
+    private fun createThumbnailData(): ThumbnailData {
         val bitmap = mock<Bitmap>()
         whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH)
         whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT)
 
-        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+        return ThumbnailData(thumbnail = bitmap)
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
index dc16475..fe67313 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
@@ -70,44 +70,6 @@
         assertThat(thumbnailDataFlow2.first()).isNull()
     }
 
-    @Test
-    fun thumbnailOverrideWaitAndReset() = runTest {
-        val thumbnailData1 = createThumbnailData().apply { snapshotId = 1 }
-        val thumbnailData2 = createThumbnailData().apply { snapshotId = 2 }
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2))
-
-        val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1)
-        val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2)
-
-        systemUnderTest.refreshAllTaskData()
-        systemUnderTest.updateVisibleTasks(listOf(1, 2))
-
-        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
-        assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2)
-
-        systemUnderTest.setRunningTaskShowScreenshot(true)
-        val thumbnailOverride = mapOf(2 to createThumbnailData().apply { snapshotId = 3 })
-        systemUnderTest.addOrUpdateThumbnailOverride(thumbnailOverride)
-
-        systemUnderTest.waitForRunningTaskShowScreenshotToUpdate()
-        val expectedUpdate = mapOf(2 to createThumbnailData().apply { snapshotId = 3 })
-        systemUnderTest.waitForThumbnailsToUpdate(expectedUpdate)
-
-        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
-        assertThat(thumbnailDataFlow2.first()?.snapshotId).isEqualTo(3)
-
-        systemUnderTest.onReset()
-
-        assertThat(thumbnailDataFlow1.first()).isNull()
-        assertThat(thumbnailDataFlow2.first()).isNull()
-
-        systemUnderTest.updateVisibleTasks(listOf(1, 2))
-
-        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
-        assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2)
-    }
-
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
             colorBackground = Color.argb(taskId, taskId, taskId, taskId)
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
index 15b1e53..d064f4a 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
@@ -57,6 +57,7 @@
     @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
     @Mock lateinit var taskbarPinningController: TaskbarPinningController
     @Mock lateinit var optionalBubbleControllers: Optional<BubbleControllers>
+    @Mock lateinit var taskbarDesktopModeController: TaskbarDesktopModeController
 
     lateinit var taskbarControllers: TaskbarControllers
 
@@ -98,6 +99,7 @@
                 keyboardQuickSwitchController,
                 taskbarPinningController,
                 optionalBubbleControllers,
+                taskbarDesktopModeController
             )
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index 2c23f86..710ad6f 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -16,8 +16,6 @@
 
 package com.android.quickstep;
 
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
 
 import static org.junit.Assert.assertNotNull;
@@ -32,8 +30,6 @@
 import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
-import com.android.launcher3.util.rule.ScreenRecordRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.After;
@@ -95,8 +91,6 @@
     @Test
     @PortraitLandscape
     @NavigationModeSwitch
-    @ScreenRecordRule.ScreenRecord // b/336606166
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/336606166
     public void switchToOverview() throws Exception {
         assumeTrue(mLauncher.isTablet());
 
diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java
index 40873be..8969b60 100644
--- a/src/com/android/launcher3/LauncherApplication.java
+++ b/src/com/android/launcher3/LauncherApplication.java
@@ -17,14 +17,23 @@
 
 import android.app.Application;
 
+import com.android.launcher3.dagger.DaggerLauncherAppComponent;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
 /**
  * Main application class for Launcher
  */
 public class LauncherApplication extends Application {
 
+    private LauncherBaseAppComponent mAppComponent;
     @Override
     public void onCreate() {
         super.onCreate();
         MainProcessInitializer.initialize(this);
+        mAppComponent = DaggerLauncherAppComponent.builder().build();
+    }
+
+    public LauncherBaseAppComponent getAppComponent() {
+        return mAppComponent;
     }
 }
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
new file mode 100644
index 0000000..3488c95
--- /dev/null
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.dagger;
+
+/**
+ * Launcher base component for Dagger injection.
+ *
+ * This class is not actually annotated as a Dagger component, since it is not used directly as one.
+ * Doing so generates unnecessary code bloat.
+ *
+ * See {@link LauncherAppComponent} for the one actually used by AOSP.
+ */
+public interface LauncherBaseAppComponent {
+    /** Builder for LauncherBaseAppComponent. */
+    interface Builder {
+        LauncherBaseAppComponent build();
+    }
+}
diff --git a/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java
new file mode 100644
index 0000000..4d7f937
--- /dev/null
+++ b/src_no_quickstep/com/android/launcher3/dagger/LauncherAppComponent.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.dagger;
+
+import dagger.Component;
+
+import javax.inject.Singleton;
+
+/**
+ * Root component for Dagger injection for Launcher AOSP.
+ */
+@Singleton
+@Component
+public interface LauncherAppComponent extends LauncherBaseAppComponent {
+    /** Builder for aosp LauncherAppComponent. */
+    @Component.Builder
+    interface Builder extends LauncherBaseAppComponent.Builder {
+        LauncherAppComponent build();
+    }
+}
+
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 907aa50..7c87c65 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -98,7 +98,6 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/311099513
     public void testUninstallFromWorkspace() throws Exception {
         installDummyAppAndWaitForUIUpdate();
         try {