Merge "Increase spring loaded hover time for tests since we switch pages too quickly leading to flakiness" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 88804b7..21b9863 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -310,3 +310,9 @@
     bug: "341795751"
 }
 
+flag {
+    name: "enable_new_archiving_icon"
+    namespace: "launcher"
+    description: "Archived apps will use new icon in app title"
+    bug: "350758155"
+}
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 7235b63..8984086 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -44,7 +44,6 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.widget.BaseWidgetSheet;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -81,6 +80,15 @@
      */
     private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets";
     /**
+     * Intent extra for the string representing the title displayed within the picker header.
+     */
+    private static final String EXTRA_PICKER_TITLE = "picker_title";
+    /**
+     * Intent extra for the string representing the description displayed within the picker header.
+     */
+    private static final String EXTRA_PICKER_DESCRIPTION = "picker_description";
+
+    /**
      * A unique identifier of the surface hosting the widgets;
      * <p>"widgets" is reserved for home screen surface.</p>
      * <p>"widgets_hub" is reserved for glanceable hub surface.</p>
@@ -108,6 +116,10 @@
     // Widgets existing on the host surface.
     @NonNull
     private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>();
+    @Nullable
+    private String mTitle;
+    @Nullable
+    private String mDescription;
 
     /** A set of user ids that should be filtered out from the selected widgets. */
     @NonNull
@@ -137,6 +149,9 @@
     }
 
     private void parseIntentExtras() {
+        mTitle = getIntent().getStringExtra(EXTRA_PICKER_TITLE);
+        mDescription = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION);
+
         // A value of 0 for either size means that no filtering will occur in that dimension. If
         // both values are 0, then no size filtering will occur.
         mDesiredWidgetWidth =
@@ -241,7 +256,7 @@
     /** Updates the model with widgets, applies filters and launches the widgets sheet once
      * widgets are available */
     private void refreshAndBindWidgets() {
-        MODEL_EXECUTOR.getHandler().postDelayed(() -> {
+        MODEL_EXECUTOR.execute(() -> {
             LauncherAppState app = LauncherAppState.getInstance(this);
             mModel.update(app, null);
             final List<WidgetsListBaseEntry> allWidgets =
@@ -271,7 +286,7 @@
                         mUiSurface, allWidgetItems);
                 mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
             }
-        }, mDeviceProfile.bottomSheetOpenDuration);
+        });
     }
 
     private void bindWidgets(List<WidgetsListBaseEntry> widgets) {
@@ -280,7 +295,8 @@
 
     private void openWidgetsSheet() {
         MAIN_EXECUTOR.execute(() -> {
-            BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true);
+            WidgetsFullSheet widgetSheet = WidgetsFullSheet.show(this, true);
+            widgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
             widgetSheet.disableNavBarScrim(true);
             widgetSheet.addOnCloseListener(this::finish);
         });
diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
index 41fcf61..8c98bab 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
@@ -59,6 +59,11 @@
 public class WidgetPredictionsRequester {
     private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20;
     private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets";
+    // container/screenid/[positionx,positiony]/[spanx,spany]
+    // Matches the format passed used by PredictionHelper; But, position and size values aren't
+    // used, so, we pass default values.
+    @VisibleForTesting
+    static final String LAUNCH_LOCATION = "workspace/1/[0,0]/[2,2]";
 
     @Nullable
     private AppPredictor mAppPredictor;
@@ -86,7 +91,7 @@
      */
     public void request(List<AppWidgetProviderInfo> existingWidgets,
             Consumer<List<ItemInfo>> callback) {
-        Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface);
+        Bundle bundle = buildBundleForPredictionSession(existingWidgets);
         Predicate<WidgetItem> filter = notOnUiSurfaceFilter(existingWidgets);
 
         MODEL_EXECUTOR.execute(() -> {
@@ -112,17 +117,14 @@
      * Returns a bundle that can be passed in a prediction session
      *
      * @param addedWidgets widgets that are already added by the user in the ui surface
-     * @param uiSurface    a unique identifier of the surface hosting widgets; format
-     *                     "widgets_xx"; note - "widgets" is reserved for home screen surface.
      */
     @VisibleForTesting
-    static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets,
-            String uiSurface) {
+    static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets) {
         Bundle bundle = new Bundle();
         ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>();
         for (AppWidgetProviderInfo info : addedWidgets) {
             ComponentName componentName = info.provider;
-            AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName);
+            AppTargetEvent appTargetEvent = buildAppTargetEvent(info, componentName);
             addedAppTargetEvents.add(appTargetEvent);
         }
         bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents);
@@ -134,13 +136,13 @@
      * predictor.
      * Also see {@link PredictionHelper}
      */
-    private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info,
+    private static AppTargetEvent buildAppTargetEvent(AppWidgetProviderInfo info,
             ComponentName componentName) {
         AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName());
         AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(),
                 /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build();
-        return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
-                .setLaunchLocation(uiSurface).build();
+        return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN).setLaunchLocation(
+                LAUNCH_LOCATION).build();
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 5d47212..0ba5de1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -46,6 +46,7 @@
 import androidx.core.content.res.ResourcesCompat;
 
 import com.android.app.animation.Interpolators;
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
@@ -53,6 +54,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 import java.util.HashMap;
 import java.util.List;
@@ -331,6 +333,8 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 super.onAnimationStart(animation);
+                InteractionJankMonitorWrapper.begin(
+                        KeyboardQuickSwitchView.this, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
                 setClipToPadding(false);
                 setOutlineProvider(new ViewOutlineProvider() {
                     @Override
@@ -366,12 +370,19 @@
             }
 
             @Override
+            public void onAnimationCancel(Animator animation) {
+                super.onAnimationCancel(animation);
+                InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
+            }
+
+            @Override
             public void onAnimationEnd(Animator animation) {
                 super.onAnimationEnd(animation);
                 setClipToPadding(true);
                 setOutlineProvider(outlineProvider);
                 invalidateOutline();
                 mOpenAnimation = null;
+                InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
             }
         });
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 73819b3..d411ba6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.view.KeyEvent;
 import android.view.animation.AnimationUtils;
 import android.window.RemoteTransition;
@@ -23,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
@@ -30,6 +32,7 @@
 import com.android.quickstep.util.SlideInRemoteTransition;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 
 import java.io.PrintWriter;
@@ -93,18 +96,28 @@
 
     protected void closeQuickSwitchView(boolean animate) {
         if (isCloseAnimationRunning()) {
-            // Let currently-running animation finish.
             if (!animate) {
                 mCloseAnimation.end();
             }
+            // Let currently-running animation finish.
             return;
         }
         if (!animate) {
+            InteractionJankMonitorWrapper.begin(
+                    mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
             onCloseComplete();
             return;
         }
         mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation();
 
+        mCloseAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                super.onAnimationStart(animation);
+                InteractionJankMonitorWrapper.begin(
+                        mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
+            }
+        });
         mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
         mCloseAnimation.start();
     }
@@ -142,16 +155,26 @@
             return -1;
         }
 
+        Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin(
+                mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
+        Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
+                Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
         TaskbarActivityContext context = mControllers.taskbarActivityContext;
         RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
                 Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
                 context.getDeviceProfile().overviewPageSpacing,
                 QuickStepContract.getWindowCornerRadius(context),
                 AnimationUtils.loadInterpolator(
-                        context, android.R.interpolator.fast_out_extra_slow_in)),
+                        context, android.R.interpolator.fast_out_extra_slow_in),
+                onStartCallback,
+                onFinishCallback),
                 "SlideInTransition");
         mControllers.taskbarActivityContext.handleGroupTaskLaunch(
-                task, remoteTransition, mOnDesktop);
+                task,
+                remoteTransition,
+                mOnDesktop,
+                onStartCallback,
+                onFinishCallback);
         return -1;
     }
 
@@ -159,6 +182,7 @@
         mCloseAnimation = null;
         mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
         mControllerCallbacks.onCloseComplete();
+        InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
     }
 
     protected void onDestroy() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 6c3b4ad..1166cf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -95,6 +95,7 @@
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.TaskItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
@@ -1132,6 +1133,11 @@
                 mControllers.uiController.onTaskbarIconLaunched(api);
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
+        } else if (tag instanceof TaskItemInfo info) {
+            UI_HELPER_EXECUTOR.execute(() ->
+                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId()));
+            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
+                    /* stash= */ true);
         } else if (tag instanceof WorkspaceItemInfo) {
             // Tapping a launchable icon on Taskbar
             WorkspaceItemInfo info = (WorkspaceItemInfo) tag;
@@ -1215,22 +1221,44 @@
         }
     }
 
+    public void handleGroupTaskLaunch(
+            GroupTask task,
+            @Nullable RemoteTransition remoteTransition,
+            boolean onDesktop) {
+        handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
+    }
+
     /**
      * Launches the given GroupTask with the following behavior:
      * - If the GroupTask is a DesktopTask, launch the tasks in that Desktop.
      * - If {@code onDesktop}, bring the given GroupTask to the front.
      * - If the GroupTask is a single task, launch it via startActivityFromRecents.
      * - Otherwise, we assume the GroupTask is a Split pair and launch them together.
+     * <p>
+     * Given start and/or finish callbacks, they will be run before an after the app launch
+     * respectively in cases where we can't use the remote transition, otherwise we will assume that
+     * these callbacks are included in the remote transition.
      */
-    public void handleGroupTaskLaunch(GroupTask task, @Nullable RemoteTransition remoteTransition,
-            boolean onDesktop) {
+    public void handleGroupTaskLaunch(
+            GroupTask task,
+            @Nullable RemoteTransition remoteTransition,
+            boolean onDesktop,
+            @Nullable Runnable onStartCallback,
+            @Nullable Runnable onFinishCallback) {
         if (task instanceof DesktopTask) {
             UI_HELPER_EXECUTOR.execute(() ->
                     SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
                             remoteTransition));
         } else if (onDesktop) {
-            UI_HELPER_EXECUTOR.execute(() ->
-                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id));
+            UI_HELPER_EXECUTOR.execute(() -> {
+                if (onStartCallback != null) {
+                    onStartCallback.run();
+                }
+                SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id);
+                if (onFinishCallback != null) {
+                    onFinishCallback.run();
+                }
+            });
         } else if (task.task2 == null) {
             UI_HELPER_EXECUTOR.execute(() -> {
                 ActivityOptions activityOptions =
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 0b7ae39..5024cd8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -196,26 +196,26 @@
         final TaskbarRecentAppsController recentAppsController =
                 mControllers.taskbarRecentAppsController;
         hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
-        Set<String> runningPackages = recentAppsController.getRunningAppPackages();
-        Set<String> minimizedPackages = recentAppsController.getMinimizedAppPackages();
+        Set<Integer> runningTaskIds = recentAppsController.getRunningTaskIds();
+        Set<Integer> minimizedTaskIds = recentAppsController.getMinimizedTaskIds();
 
         if (mDeferUpdatesForSUW) {
             ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
             mDeferredUpdates = () ->
                     commitHotseatItemUpdates(finalHotseatItemInfos,
-                            recentAppsController.getShownTasks(), runningPackages,
-                            minimizedPackages);
+                            recentAppsController.getShownTasks(), runningTaskIds,
+                            minimizedTaskIds);
         } else {
             commitHotseatItemUpdates(hotseatItemInfos,
-                    recentAppsController.getShownTasks(), runningPackages, minimizedPackages);
+                    recentAppsController.getShownTasks(), runningTaskIds, minimizedTaskIds);
         }
     }
 
     private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
-            Set<String> runningPackages, Set<String> minimizedPackages) {
+            Set<Integer> runningTaskIds, Set<Integer> minimizedTaskIds) {
         mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
         mControllers.taskbarViewController.updateIconViewsRunningStates(
-                runningPackages, minimizedPackages);
+                runningTaskIds, minimizedTaskIds);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index d26a36d..ea091ca 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -166,8 +166,12 @@
         if (buttonType == BUTTON_SPACE) {
             return false;
         }
-        // Provide the same haptic feedback that the system offers for virtual keys.
-        view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+
+        // Provide the same haptic feedback that the system offers for long press.
+        // The haptic feedback from long pressing on the home button is handled by circle to search.
+        if (buttonType != BUTTON_HOME) {
+            view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        }
         switch (buttonType) {
             case BUTTON_HOME:
                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS);
@@ -179,10 +183,12 @@
                 return true;
             case BUTTON_BACK:
                 logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
-                return backRecentsLongpress(buttonType);
+                backRecentsLongpress(buttonType);
+                return true;
             case BUTTON_RECENTS:
                 logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS);
-                return backRecentsLongpress(buttonType);
+                backRecentsLongpress(buttonType);
+                return true;
             case BUTTON_IME_SWITCH:
             default:
                 return false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 36828a8..5c08116 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -18,6 +18,8 @@
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.TaskItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
 import com.android.launcher3.util.CancellableTask
@@ -58,9 +60,13 @@
     // Initialized in init.
     private lateinit var controllers: TaskbarControllers
 
-    private var shownHotseatItems: List<ItemInfo> = emptyList()
+    var shownHotseatItems: List<ItemInfo> = emptyList()
+        private set
+
     private var allRecentTasks: List<GroupTask> = emptyList()
     private var desktopTask: DesktopTask? = null
+    // Keeps track of the order in which running tasks appear.
+    private var orderedRunningTaskIds = emptyList<Int>()
     var shownTasks: List<GroupTask> = emptyList()
         private set
 
@@ -70,9 +76,9 @@
     private val isInDesktopMode: Boolean
         get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
 
-    val runningAppPackages: Set<String>
+    val runningTaskIds: Set<Int>
         /**
-         * Returns the package names of apps that should be indicated as "running" to the user.
+         * Returns the task IDs of apps that should be indicated as "running" to the user.
          * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet().
          */
         get() {
@@ -80,22 +86,19 @@
                 return emptySet()
             }
             val tasks = desktopTask?.tasks ?: return emptySet()
-            return tasks.map { task -> task.key.packageName }.toSet()
+            return tasks.map { task -> task.key.id }.toSet()
         }
 
-    val minimizedAppPackages: Set<String>
+    val minimizedTaskIds: Set<Int>
         /**
-         * Returns the package names of apps that should be indicated as "minimized" to the user.
-         * Specifically, we return all the running packages where all the tasks in that package are
-         * minimized (not visible).
+         * Returns the task IDs for the tasks that should be indicated as "minimized" to the user.
          */
         get() {
             if (!canShowRunningApps || !isInDesktopMode) {
                 return emptySet()
             }
             val desktopTasks = desktopTask?.tasks ?: return emptySet()
-            val packageToTasks = desktopTasks.groupBy { it.key.packageName }
-            return packageToTasks.filterValues { tasks -> tasks.all { !it.isVisible } }.keys
+            return desktopTasks.filter { !it.isVisible }.map { task -> task.key.id }.toSet()
         }
 
     private val recentTasksChangedListener =
@@ -137,25 +140,39 @@
                 .filter { itemInfo -> !itemInfo.isPredictedItem }
                 .toMutableList()
 
+        if (isInDesktopMode && canShowRunningApps) {
+            shownHotseatItems =
+                updateHotseatItemsFromRunningTasks(
+                    getOrderedAndWrappedDesktopTasks(),
+                    shownHotseatItems
+                )
+        }
+
         onRecentsOrHotseatChanged()
 
         return shownHotseatItems.toTypedArray()
     }
 
+    private fun getOrderedAndWrappedDesktopTasks(): List<GroupTask> {
+        val tasks = desktopTask?.tasks ?: emptyList()
+        // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
+        val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index }
+        val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] })
+        return sortedTasks.map { GroupTask(it) }
+    }
+
     private fun reloadRecentTasksIfNeeded() {
         if (!recentsModel.isTaskListValid(taskListChangeId)) {
             taskListChangeId =
                 recentsModel.getTasks { tasks ->
                     allRecentTasks = tasks
-                    val oldRunningPackages = runningAppPackages
-                    val oldMinimizedPackages = minimizedAppPackages
+                    val oldRunningTaskdIds = runningTaskIds
+                    val oldMinimizedTaskIds = minimizedTaskIds
                     desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
-                    val runningPackagesChanged = oldRunningPackages != runningAppPackages
-                    val minimizedPackagessChanged = oldMinimizedPackages != minimizedAppPackages
+                    val runningTasksChanged = oldRunningTaskdIds != runningTaskIds
+                    val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds
                     if (
-                        onRecentsOrHotseatChanged() ||
-                            runningPackagesChanged ||
-                            minimizedPackagessChanged
+                        onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged
                     ) {
                         controllers.taskbarViewController.commitRunningAppsToUI()
                     }
@@ -170,6 +187,7 @@
      */
     private fun onRecentsOrHotseatChanged(): Boolean {
         val oldShownTasks = shownTasks
+        orderedRunningTaskIds = updateOrderedRunningTaskIds()
         shownTasks =
             if (isInDesktopMode) {
                 computeShownRunningTasks()
@@ -201,22 +219,39 @@
         return shownTasksChanged
     }
 
+    private fun updateOrderedRunningTaskIds(): MutableList<Int> {
+        val desktopTaskAsList = getOrderedAndWrappedDesktopTasks()
+        val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id }
+        var newOrder =
+            orderedRunningTaskIds
+                .filter { it in desktopTaskIds } // Only keep the tasks that are still running
+                .toMutableList()
+        // Add new tasks not already listed
+        newOrder.addAll(desktopTaskIds.filter { it !in newOrder })
+        return newOrder
+    }
+
     private fun computeShownRunningTasks(): List<GroupTask> {
         if (!canShowRunningApps) {
             return emptyList()
         }
-        val tasks = desktopTask?.tasks ?: emptyList()
-        // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
-        var desktopTaskAsList = tasks.map { GroupTask(it) }
-        // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too.
-        desktopTaskAsList = dedupeHotseatTasks(desktopTaskAsList, shownHotseatItems)
-        val desktopPackages = desktopTaskAsList.map { it.packageNames }
-        // Remove any missing Tasks.
-        val newShownTasks = shownTasks.filter { it.packageNames in desktopPackages }.toMutableList()
-        val newShownPackages = newShownTasks.map { it.packageNames }
+        val desktopTaskAsList = getOrderedAndWrappedDesktopTasks()
+        val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id }
+        val shownTaskIds = shownTasks.map { it.task1.key.id }
+        // TODO(b/315344726 Multi-instance support): only show one icon per package once we support
+        //  taskbar multi-instance menus
+        val shownHotseatItemTaskIds =
+            shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId }
+        // Remove any newly-missing Tasks, and actual group-tasks
+        val newShownTasks =
+            shownTasks
+                .filter { !it.hasMultipleTasks() }
+                .filter { it.task1.key.id in desktopTaskIds }
+                .toMutableList()
         // Add any new Tasks, maintaining the order from previous shownTasks.
-        newShownTasks.addAll(desktopTaskAsList.filter { it.packageNames !in newShownPackages })
-        return newShownTasks.toList()
+        newShownTasks.addAll(desktopTaskAsList.filter { it.task1.key.id !in shownTaskIds })
+        // Remove any tasks already covered by Hotseat icons
+        return newShownTasks.filter { it.task1.key.id !in shownHotseatItemTaskIds }
     }
 
     private fun computeShownRecentTasks(): List<GroupTask> {
@@ -245,6 +280,25 @@
         }
     }
 
+    /**
+     * Returns the hotseat items updated so that any item that points to a package with a running
+     * task also references that task.
+     */
+    private fun updateHotseatItemsFromRunningTasks(
+        groupTasks: List<GroupTask>,
+        shownHotseatItems: List<ItemInfo>
+    ): List<ItemInfo> =
+        shownHotseatItems.map { itemInfo ->
+            if (itemInfo is TaskItemInfo) {
+                itemInfo
+            } else {
+                val foundTask =
+                    groupTasks.find { task -> task.task1.key.packageName == itemInfo.targetPackage }
+                        ?: return@map itemInfo
+                TaskItemInfo(foundTask.task1.key.id, itemInfo as WorkspaceItemInfo)
+            }
+        }
+
     override fun dumpLogs(prefix: String, pw: PrintWriter) {
         pw.println("$prefix TaskbarRecentAppsController:")
         pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
@@ -253,8 +307,8 @@
         pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}")
         pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}")
         pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}")
-        pw.println("$prefix\trunningTasks=$runningAppPackages")
-        pw.println("$prefix\tminimizedTasks=$minimizedAppPackages")
+        pw.println("$prefix\trunningTaskIds=$runningTaskIds")
+        pw.println("$prefix\tminimizedTaskIds=$minimizedTaskIds")
     }
 
     private val GroupTask.packageNames: List<String>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index e59a016..527e3a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.TaskItemInfo;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
@@ -515,35 +516,38 @@
         return mTaskbarView.getTaskbarDividerView();
     }
 
-    /** Updates which icons are marked as running given the Set of currently running packages. */
-    public void updateIconViewsRunningStates(Set<String> runningPackages,
-            Set<String> minimizedPackages) {
+    /**
+     * Updates which icons are marked as running or minimized given the Sets of currently running
+     * and minimized tasks.
+     */
+    public void updateIconViewsRunningStates(Set<Integer> runningTaskIds,
+            Set<Integer> minimizedTaskIds) {
         for (View iconView : getIconViews()) {
             if (iconView instanceof BubbleTextView btv) {
                 btv.updateRunningState(
-                        getRunningAppState(btv, runningPackages, minimizedPackages));
+                        getRunningAppState(btv, runningTaskIds, minimizedTaskIds));
             }
         }
     }
 
     private BubbleTextView.RunningAppState getRunningAppState(
             BubbleTextView btv,
-            Set<String> runningPackages,
-            Set<String> minimizedPackages) {
+            Set<Integer> runningTaskIds,
+            Set<Integer> minimizedTaskIds) {
         Object tag = btv.getTag();
-        if (tag instanceof ItemInfo itemInfo) {
-            if (minimizedPackages.contains(itemInfo.getTargetPackage())) {
+        if (tag instanceof TaskItemInfo itemInfo) {
+            if (minimizedTaskIds.contains(itemInfo.getTaskId())) {
                 return BubbleTextView.RunningAppState.MINIMIZED;
             }
-            if (runningPackages.contains(itemInfo.getTargetPackage())) {
+            if (runningTaskIds.contains(itemInfo.getTaskId())) {
                 return BubbleTextView.RunningAppState.RUNNING;
             }
         }
         if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
-            if (minimizedPackages.contains(groupTask.task1.key.getPackageName())) {
+            if (minimizedTaskIds.contains(groupTask.task1.key.id)) {
                 return BubbleTextView.RunningAppState.MINIMIZED;
             }
-            if (runningPackages.contains(groupTask.task1.key.getPackageName())) {
+            if (runningTaskIds.contains(groupTask.task1.key.id)) {
                 return BubbleTextView.RunningAppState.RUNNING;
             }
         }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 867533c..f020c8f 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -152,8 +152,6 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
 
-import kotlin.Unit;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -163,6 +161,8 @@
 import java.util.OptionalInt;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
@@ -2423,34 +2423,42 @@
         if (mRecentsAnimationController == null) {
             return;
         }
+        final Runnable onFinishComplete = () -> {
+            ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                    "AbsSwipeUpHandler.onTasksAppeared: ")
+                    .append("force finish recents animation complete; clearing state callback."));
+            mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+        };
+        ActiveGestureLog.CompoundString forceFinishReason = new ActiveGestureLog.CompoundString(
+                "Forcefully finishing recents animation: ");
         if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED)
                 && !hasStartedTaskBefore(appearedTaskTargets)) {
             // This is a special case, if a task is started mid-gesture that wasn't a part of a
             // previous quickswitch task launch, then cancel the animation back to the app
             RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
             TaskInfo taskInfo = appearedTaskTarget.taskInfo;
-            ActiveGestureLog.INSTANCE.addLog(
-                    new ActiveGestureLog.CompoundString("Unexpected task appeared")
-                            .append(" id=")
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason
+                            .append("Unexpected task appeared id=")
                             .append(taskInfo.taskId)
                             .append(" pkg=")
                             .append(taskInfo.baseIntent.getComponent().getPackageName()));
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         ActiveGestureLog.CompoundString handleTaskFailureReason =
                 new ActiveGestureLog.CompoundString("handleTaskAppeared check failed: ");
         if (!handleTaskAppeared(appearedTaskTargets, handleTaskFailureReason)) {
-            ActiveGestureLog.INSTANCE.addLog(handleTaskFailureReason);
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append(handleTaskFailureReason));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         RemoteAnimationTarget[] taskTargets = Arrays.stream(appearedTaskTargets)
                 .filter(mGestureState.mLastStartedTaskIdPredicate)
                 .toArray(RemoteAnimationTarget[]::new);
         if (taskTargets.length == 0) {
-            ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            ActiveGestureLog.INSTANCE.addLog(
+                    forceFinishReason.append("No appeared task matching started task id"));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         RemoteAnimationTarget taskTarget = taskTargets[0];
@@ -2458,13 +2466,13 @@
                 ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
         if (taskView == null || taskView.getTaskContainers().stream().noneMatch(
                 TaskContainer::getShouldShowSplashView)) {
-            ActiveGestureLog.INSTANCE.addLog("Splash not needed");
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Splash not needed"));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         if (mContainer == null) {
-            ActiveGestureLog.INSTANCE.addLog("Activity destroyed");
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Activity destroyed"));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         animateSplashScreenExit(mContainer, appearedTaskTargets, taskTargets);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index a7d3890..f902284 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -302,6 +302,10 @@
         return mNavBarPosition;
     }
 
+    public NavigationMode getMode() {
+        return mMode;
+    }
+
     /**
      * @return whether the current nav mode is fully gestural.
      */
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index ba33c62..f813d9a 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -26,6 +26,7 @@
 import android.graphics.Matrix.ScaleToFit;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Log;
 import android.view.RemoteAnimationTarget;
 
 import androidx.annotation.NonNull;
@@ -381,6 +382,8 @@
     protected class SpringAnimationRunner extends AnimationSuccessListener
             implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
 
+        private static final String TAG = "SpringAnimationRunner";
+
         final Rect mCropRect = new Rect();
         final Matrix mMatrix = new Matrix();
 
@@ -481,10 +484,26 @@
                 return;
             }
             mTargetTaskView.setAlpha(mAnimationFactory.isAnimatingIntoIcon() ? 1f : alpha);
-            float width = mThumbnailStartBounds.width();
-            float height =  mThumbnailStartBounds.height();
-            float scale = Math.min(currentRect.width(), currentRect.height())
-                    / Math.min(width, height);
+            float startWidth = mThumbnailStartBounds.width();
+            float startHeight =  mThumbnailStartBounds.height();
+            float currentWidth = currentRect.width();
+            float currentHeight = currentRect.height();
+            float scale;
+
+            boolean isStartWidthValid = Float.compare(startWidth, 0f) > 0;
+            boolean isStartHeightValid = Float.compare(startHeight, 0f) > 0;
+            if (isStartWidthValid && isStartHeightValid) {
+                scale = Math.min(currentWidth, currentHeight) / Math.min(startWidth, startHeight);
+            } else {
+                Log.e(TAG, "TaskView starting bounds are invalid: " + mThumbnailStartBounds);
+                if (isStartWidthValid) {
+                    scale = currentWidth / startWidth;
+                } else if (isStartHeightValid) {
+                    scale = currentHeight / startHeight;
+                } else {
+                    scale = 1f;
+                }
+            }
 
             mTargetTaskView.setScaleX(scale);
             mTargetTaskView.setScaleY(scale);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index ee93cd6..2896979 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -42,6 +42,7 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
@@ -70,7 +71,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.util.ArraySet;
@@ -102,6 +102,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.LockedUserState;
+import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.PluginManagerWrapper;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.ScreenOnTracker;
@@ -622,6 +623,8 @@
     private InputManager mInputManager;
     private final Set<Integer> mTrackpadsConnected = new ArraySet<>();
 
+    private NavigationMode mGestureStartNavMode = null;
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -836,14 +839,26 @@
         TestLogging.recordMotionEvent(
                 TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
 
-        boolean isUserUnlocked = LockedUserState.get(this).isUserUnlocked();
-        if (!isUserUnlocked || (mDeviceState.isButtonNavMode()
-                && !isTrackpadMotionEvent(event))) {
+        if (!LockedUserState.get(this).isUserUnlocked()) {
+            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
+                    .append("Cannot process input event: user is locked"));
+            return;
+        }
+
+        NavigationMode currentNavMode = mDeviceState.getMode();
+        if (mGestureStartNavMode != null && mGestureStartNavMode != currentNavMode) {
+            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
+                            .append("Navigation mode switched mid-gesture (")
+                            .append(mGestureStartNavMode.name())
+                            .append(" -> ")
+                            .append(currentNavMode.name())
+                            .append("); cancelling gesture."),
+                    NAVIGATION_MODE_SWITCHED);
+            event.setAction(ACTION_CANCEL);
+        } else if (mDeviceState.isButtonNavMode() && !isTrackpadMotionEvent(event)) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
                     .append("Cannot process input event: ")
-                    .append(!isUserUnlocked
-                            ? "user is locked"
-                            : "using 3-button nav and event is not a trackpad event"));
+                    .append("using 3-button nav and event is not a trackpad event"));
             return;
         }
 
@@ -870,6 +885,12 @@
             }
         }
 
+        if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
+            mGestureStartNavMode = currentNavMode;
+        } else if (action == ACTION_UP || action == ACTION_CANCEL) {
+            mGestureStartNavMode = null;
+        }
+
         SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
 
         CompoundString reasonString = action == ACTION_DOWN
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 3140fff..2398e66 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -40,7 +40,7 @@
         SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
         FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
         INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
-        QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED,
+        QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED, NAVIGATION_MODE_SWITCHED,
 
         /**
          * These GestureEvents are specifically associated to state flags that get set in
@@ -299,6 +299,13 @@
                                     + "the current page index and index 0 were missing.",
                             writer);
                     break;
+                case NAVIGATION_MODE_SWITCHED:
+                    errorDetected |= printErrorIfTrue(
+                            true,
+                            prefix,
+                            /* errorMessage= */ "Navigation mode switched mid-gesture.",
+                            writer);
+                    break;
                 case EXPECTING_TASK_APPEARED:
                 case MOTION_DOWN:
                 case SET_END_TARGET:
diff --git a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
index dbeedd3..ece9583 100644
--- a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
+++ b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
@@ -36,6 +36,8 @@
     private val pageSpacing: Int,
     private val cornerRadius: Float,
     private val interpolator: TimeInterpolator,
+    private val onStartCallback: Runnable,
+    private val onFinishCallback: Runnable,
 ) : RemoteTransitionStub() {
     private val animationDurationMs = 500L
 
@@ -68,6 +70,7 @@
                 startT.setCrop(leash, chg.endAbsBounds).setCornerRadius(leash, cornerRadius)
             }
         }
+        onStartCallback.run()
         startT.apply()
 
         anim.addUpdateListener {
@@ -97,6 +100,7 @@
                     val t = Transaction()
                     try {
                         finishCB.onTransitionFinished(null, t)
+                        onFinishCallback.run()
                     } catch (e: RemoteException) {
                         // Ignore
                     }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 49e1c88..0cd36f4 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -27,8 +27,10 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.content.Context
 import android.graphics.Bitmap
+import android.graphics.Color
 import android.graphics.Rect
 import android.graphics.RectF
+import android.graphics.drawable.ColorDrawable
 import android.graphics.drawable.Drawable
 import android.view.RemoteAnimationTarget
 import android.view.SurfaceControl
@@ -121,7 +123,7 @@
                     return SplitAnimInitProps(
                         container.snapshotView,
                         container.thumbnail,
-                        drawable!!,
+                        drawable,
                         fadeWithThumbnail = true,
                         isStagedTask = true,
                         iconView = container.iconView.asView()
@@ -140,7 +142,7 @@
                 return SplitAnimInitProps(
                     it.snapshotView,
                     it.thumbnail,
-                    drawable!!,
+                    drawable,
                     fadeWithThumbnail = true,
                     isStagedTask = true,
                     iconView = it.iconView.asView()
@@ -152,15 +154,15 @@
     /**
      * Returns the drawable that's provided in iconView, however if that is null it falls back to
      * the drawable that's in splitSelectSource. TaskView's icon drawable can be null if the
-     * TaskView is scrolled far enough off screen
+     * TaskView is scrolled far enough off screen.
      *
-     * @return [Drawable]
+     * @return the [Drawable] icon, or a translucent drawable if none was found
      */
-    fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable? {
-        if (iconView.drawable == null && splitSelectSource != null) {
-            return splitSelectSource.drawable
-        }
-        return iconView.drawable
+    fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable {
+        val drawable =
+            if (iconView.drawable == null && splitSelectSource != null) splitSelectSource.drawable
+            else iconView.drawable
+        return drawable ?: ColorDrawable(Color.TRANSPARENT)
     }
 
     /**
@@ -534,7 +536,8 @@
             val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info)
             if (appPairLaunchingAppIndex == -1) {
                 // Launch split app pair animation
-                composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+                composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback,
+                        cornerRadius)
             } else {
                 composeFullscreenIconSplitLaunchAnimator(
                     launchingIconView,
@@ -652,15 +655,15 @@
      * To find the root shell leash that we want to fade in, we do the following: The Changes we
      * receive in transitionInfo are structured like this
      *
-     *     Root (grandparent)
+     *     (0) Root (grandparent)
      *     |
-     *     |--> Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |--> (1) Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW)
      *     |   |
-     *     |    --> App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |    --> (1a) App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW)
      *     |--> Divider
-     *     |--> Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |--> (2) Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW)
      *         |
-     *          --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
+     *          --> (2a) App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
      *
      * We want to animate the Root (grandparent) so that it affects both apps and the divider. To do
      * this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the left-side ones,
@@ -675,7 +678,8 @@
         launchingIconView: AppPairIcon,
         transitionInfo: TransitionInfo,
         t: Transaction,
-        finishCallback: Runnable
+        finishCallback: Runnable,
+        windowRadius: Float
     ) {
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
@@ -695,48 +699,25 @@
 
         // Create an AnimatorSet that will run both shell and launcher transitions together
         val launchAnimation = AnimatorSet()
-        var rootCandidate: Change? = null
 
-        for (change in transitionInfo.changes) {
-            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+        val splitRoots: Pair<Change, List<Change>>? =
+                SplitScreenUtils.extractTopParentAndChildren(transitionInfo)
+        check(splitRoots != null) { "Could not find split roots" }
 
-            // TODO (b/316490565): Replace this logic when SplitBounds is available to
-            //  startAnimation() and we can know the precise taskIds of launching tasks.
-            // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
-            if (
-                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
-                    (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
-            ) {
-                // Check if it is a left/top app.
-                val isLeftTopApp =
-                    (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
-                        (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
-                if (isLeftTopApp) {
-                    // Found one!
-                    rootCandidate = change
-                    break
-                }
-            }
-        }
-
-        // If we could not find a proper root candidate, something went wrong.
-        check(rootCandidate != null) { "Could not find a split root candidate" }
+        // Will point to change (0) in diagram above
+        val mainRootCandidate = splitRoots.first
+        // Will contain changes (1) and (2) in diagram above
+        val leafRoots: List<Change> = splitRoots.second
 
         // Find the place where our left/top app window meets the divider (used for the
         // launcher side animation)
-        val dividerPos =
-            if (dp.isLeftRightSplit) rootCandidate.endAbsBounds.right
-            else rootCandidate.endAbsBounds.bottom
-
-        // Recurse up the tree until parent is null, then we've found our root.
-        var parentToken: WindowContainerToken? = rootCandidate.parent
-        while (parentToken != null) {
-            rootCandidate = transitionInfo.getChange(parentToken) ?: break
-            parentToken = rootCandidate.parent
+        val leftTopApp = leafRoots.single { change ->
+            (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
+                    (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
         }
-
-        // Make sure nothing weird happened, like getChange() returning null.
-        check(rootCandidate != null) { "Failed to find a root leash" }
+        val dividerPos =
+            if (dp.isLeftRightSplit) leftTopApp.endAbsBounds.right
+            else leftTopApp.endAbsBounds.bottom
 
         // Create a new floating view in Launcher, positioned above the launching icon
         val drawableArea = launchingIconView.iconDrawableArea
@@ -755,9 +736,19 @@
             )
         floatingView.bringToFront()
 
-        launchAnimation.play(
-            getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate)
+        val iconLaunchValueAnimator = getIconLaunchValueAnimator(t, dp, finishCallback, launcher,
+                floatingView, mainRootCandidate)
+        iconLaunchValueAnimator.addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
+                        for (c in leafRoots) {
+                            t.setCornerRadius(c.leash, windowRadius)
+                            t.apply()
+                        }
+                    }
+                }
         )
+        launchAnimation.play(iconLaunchValueAnimator)
         launchAnimation.start()
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index 4820c35..d58cb91 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -16,11 +16,21 @@
 
 package com.android.quickstep.util
 
+import android.util.Log
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.TransitionInfo.FLAG_FIRST_CUSTOM
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.wm.shell.util.SplitBounds
+import java.lang.IllegalStateException
 
 class SplitScreenUtils {
     companion object {
+        private const val TAG = "SplitScreenUtils"
+
         // TODO(b/254378592): Remove these methods when the two classes are reunited
         /** Converts the shell version of SplitBounds to the launcher version */
         @JvmStatic
@@ -39,5 +49,51 @@
                 )
             }
         }
+
+        /**
+         * Given a TransitionInfo, generates the tree structure for those changes and extracts out
+         * the top most root and it's two immediate children.
+         * Changes can be provided in any order.
+         *
+         * @return a [Pair] where first -> top most split root,
+         *         second -> [List] of 2, leftTop/bottomRight stage roots
+         */
+        fun extractTopParentAndChildren(transitionInfo: TransitionInfo):
+                Pair<Change, List<Change>>? {
+            val parentToChildren = mutableMapOf<Change, MutableList<Change>>()
+            val hasParent = mutableSetOf<Change>()
+            // filter out anything that isn't opening and the divider
+            val taskChanges: List<Change> = transitionInfo.changes
+                    .filter { change -> (change.mode == TRANSIT_OPEN ||
+                            change.mode == TRANSIT_TO_FRONT) && change.flags < FLAG_FIRST_CUSTOM}
+                    .toList()
+
+            // 1. Build Parent-Child Relationships
+            for (change in taskChanges) {
+                // TODO (b/316490565): Replace this logic when SplitBounds is available to
+                //  startAnimation() and we can know the precise taskIds of launching tasks.
+                change.parent?.let { parent ->
+                    parentToChildren
+                            .getOrPut(transitionInfo.getChange(parent)!!) { mutableListOf() }
+                            .add(change)
+                    hasParent.add(change)
+                }
+            }
+
+            // 2. Find Top Parent
+            val topParent = taskChanges.firstOrNull { it !in hasParent }
+
+            // 3. Extract Immediate Children
+            return if (topParent != null) {
+                val immediateChildren = parentToChildren.getOrDefault(topParent, emptyList())
+                if (immediateChildren.size != 2) {
+                    throw IllegalStateException("incorrect split stage root size")
+                }
+                Pair(topParent, immediateChildren)
+            } else {
+                Log.w(TAG, "No top parent found")
+                null
+            }
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index e024995..6bbd6b2 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -57,6 +57,7 @@
 
     private val container: RecentsViewContainer
     private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+    private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     // Animation interpolators
     protected val expandXInterpolator: Interpolator
@@ -105,13 +106,15 @@
             )
 
         // Find device-specific measurements
-        deviceCornerRadius = QuickStepContract.getWindowCornerRadius(container.asContext())
+        val resources = context.resources
+        deviceCornerRadius = QuickStepContract.getWindowCornerRadius(context)
         deviceHalfDividerSize =
-                container.asContext().resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
+                resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
         val dividerCenterPos = dividerPos + deviceHalfDividerSize
         desiredSplitRatio =
             if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx
             else dividerCenterPos / dp.heightPx
+        dividerPaint.color = resources.getColor(R.color.taskbar_background_dark, null /*theme*/)
     }
 
     override fun draw(canvas: Canvas) {
@@ -153,8 +156,12 @@
         val leftSide = RectF(0f, 0f, dividerCenterPos - changingDividerSize, height)
         // The right half of the background image
         val rightSide = RectF(dividerCenterPos + changingDividerSize, 0f, width, height)
+        // Middle part is for divider background
+        val middleRect = RectF(leftSide.right - deviceHalfDividerSize, 0f,
+                rightSide.left + deviceHalfDividerSize, height)
 
         // Draw background
+        canvas.drawRect(middleRect, dividerPaint)
         drawCustomRoundedRect(
             canvas,
             leftSide,
@@ -251,8 +258,12 @@
         val topSide = RectF(0f, 0f, width, dividerCenterPos - changingDividerSize)
         // The bottom half of the background image
         val bottomSide = RectF(0f, dividerCenterPos + changingDividerSize, width, height)
+        // Middle part is for divider background
+        val middleRect = RectF(0f, topSide.bottom - deviceHalfDividerSize,
+                width, bottomSide.top + deviceHalfDividerSize)
 
         // Draw background
+        canvas.drawRect(middleRect, dividerPaint)
         drawCustomRoundedRect(
             canvas,
             topSide,
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
index 039dce4..4ea74df 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
@@ -30,6 +30,7 @@
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetPredictionsRequester.LAUNCH_LOCATION
 import com.android.launcher3.model.WidgetPredictionsRequester.buildBundleForPredictionSession
 import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions
 import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter
@@ -103,7 +104,7 @@
     fun buildBundleForPredictionSession_includesAddedAppWidgets() {
         val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo, widget2Info)
 
-        val bundle = buildBundleForPredictionSession(existingWidgets, TEST_UI_SURFACE)
+        val bundle = buildBundleForPredictionSession(existingWidgets)
         val addedWidgetsBundleExtra =
             bundle.getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, AppTarget::class.java)
 
@@ -213,7 +214,7 @@
                     .setClassName(providerClassName)
                     .build()
             return AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
-                .setLaunchLocation(TEST_UI_SURFACE)
+                .setLaunchLocation(LAUNCH_LOCATION)
                 .build()
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index fd7ecb0..a9f5dcd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -276,7 +276,7 @@
         whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
         doNothing()
             .whenever(spySplitAnimationController)
-            .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+            .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any())
         doReturn(-1).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any())
 
         spySplitAnimationController.playSplitLaunchAnimation(
@@ -296,7 +296,7 @@
         )
 
         verify(spySplitAnimationController)
-            .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+            .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any())
     }
 
     @Test
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 13c4f72..27e761a 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -26,6 +26,7 @@
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.TaskItemInfo
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RecentsModel.RecentTasksChangedListener
@@ -78,6 +79,13 @@
         val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
         verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
         recentTasksChangedListener = listenerCaptor.value
+
+        // Make sure updateHotseatItemInfos() is called after commitRunningAppsToUI()
+        whenever(taskbarViewController.commitRunningAppsToUI()).then {
+            recentAppsController.updateHotseatItemInfos(
+                recentAppsController.shownHotseatItems.toTypedArray()
+            )
+        }
     }
 
     @Test
@@ -88,7 +96,7 @@
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
@@ -103,7 +111,7 @@
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
@@ -117,7 +125,7 @@
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
@@ -126,13 +134,58 @@
     }
 
     @Test
+    fun updateHotseatItemInfos_inDesktopMode_hotseatPackageHasRunningTask_hotseatItemLinksToTask() {
+        setInDesktopMode(true)
+
+        val newHotseatItems =
+            prepareHotseatAndRunningAndRecentApps(
+                hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+                runningTasks = listOf(createTask(id = 1, HOTSEAT_PACKAGE_1)),
+                recentTaskPackages = emptyList()
+            )
+
+        assertThat(newHotseatItems).hasLength(2)
+        assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+        assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java)
+        val hotseatItem1 = newHotseatItems[0] as TaskItemInfo
+        assertThat(hotseatItem1.taskId).isEqualTo(1)
+    }
+
+    @Test
+    fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_hotseatCoversFirstTask() {
+        setInDesktopMode(true)
+
+        val newHotseatItems =
+            prepareHotseatAndRunningAndRecentApps(
+                hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+                runningTasks =
+                    listOf(
+                        createTask(id = 1, HOTSEAT_PACKAGE_1),
+                        createTask(id = 2, HOTSEAT_PACKAGE_1)
+                    ),
+                recentTaskPackages = emptyList()
+            )
+
+        // First task is in Hotseat Items
+        assertThat(newHotseatItems).hasLength(2)
+        assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+        assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java)
+        val hotseatItem1 = newHotseatItems[0] as TaskItemInfo
+        assertThat(hotseatItem1.taskId).isEqualTo(1)
+        // Second task is in shownTasks
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks)
+            .containsExactlyElementsIn(listOf(createTask(id = 2, HOTSEAT_PACKAGE_1)))
+    }
+
+    @Test
     fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() {
         recentAppsController.canShowRecentApps = true
         setInDesktopMode(false)
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
@@ -146,7 +199,11 @@
         setInDesktopMode(true)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-            runningTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+            runningTasks =
+                listOf(
+                    createTask(id = 1, RUNNING_APP_PACKAGE_1),
+                    createTask(id = 2, RUNNING_APP_PACKAGE_2)
+                ),
             recentTaskPackages = emptyList()
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
@@ -158,7 +215,7 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
@@ -169,11 +226,15 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks =
+                listOf(
+                    createTask(id = 1, RUNNING_APP_PACKAGE_1),
+                    createTask(id = 2, RUNNING_APP_PACKAGE_2)
+                ),
             recentTaskPackages = emptyList()
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
@@ -181,7 +242,7 @@
         setInDesktopMode(true)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
@@ -190,120 +251,161 @@
     @Test
     fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
-    }
-
-    @Test
-    fun onRecentTasksChanged_inDesktopMode_runningAppIsHotseatItem_shownTasks_returnsDistinctItems() {
-        setInDesktopMode(true)
-        prepareHotseatAndRunningAndRecentApps(
-            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
-            runningTaskPackages =
-                listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
-            recentTaskPackages = emptyList()
-        )
-        val expectedPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
-        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).containsExactlyElementsIn(listOf(task1, task2))
     }
 
     @Test
     fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() {
         setInDesktopMode(false)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages).isEmpty()
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).isEmpty()
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
     fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages)
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
     fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() {
         setInDesktopMode(true)
-        val runningTaskPackages =
-            listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val runningTasks =
+            listOf(
+                createTask(id = 1, HOTSEAT_PACKAGE_1),
+                createTask(id = 2, RUNNING_APP_PACKAGE_1),
+                createTask(id = 3, RUNNING_APP_PACKAGE_2)
+            )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = runningTasks,
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages)
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2, 3))
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
-    fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() {
+    fun onRecentTasksChanged_inDesktopMode_allAppsRunningAndInvisibleAppsMinimized() {
         setInDesktopMode(true)
-        val runningTaskPackages =
-            listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
-        val minimizedTaskIndices = setOf(2) // RUNNING_APP_PACKAGE_3
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task3Minimized = createTask(id = 3, RUNNING_APP_PACKAGE_3, isVisible = false)
+        val runningTasks = listOf(task1, task2, task3Minimized)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
-            minimizedTaskIndices = minimizedTaskIndices,
+            runningTasks = runningTasks,
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages)
-        assertThat(recentAppsController.minimizedAppPackages).containsExactly(RUNNING_APP_PACKAGE_3)
+        assertThat(recentAppsController.runningTaskIds).containsExactly(1, 2, 3)
+        assertThat(recentAppsController.minimizedTaskIds).containsExactly(3)
     }
 
     @Test
-    fun getMinimizedApps_inDesktopMode_twoTasksSamePackageOneMinimizedReturnsNotMinimized() {
+    fun onRecentTasksChanged_inDesktopMode_samePackage_differentTasks_severalRunningTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_1)
-        val minimizedTaskIndices = setOf(1) // The second RUNNING_APP_PACKAGE_1 task.
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
-            minimizedTaskIndices = minimizedTaskIndices,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages.toSet())
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
     }
 
     @Test
     fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() {
         setInDesktopMode(true)
-        val originalOrder = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = originalOrder,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
+
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task2, task1),
             recentTaskPackages = emptyList()
         )
-        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).isEqualTo(originalOrder)
+
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).isEqualTo(listOf(task1, task2))
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_multiInstance_shownTasks_maintainsOrder() {
+        setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(task1, task2),
+            recentTaskPackages = emptyList()
+        )
+
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(task2, task1),
+            recentTaskPackages = emptyList()
+        )
+
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).isEqualTo(listOf(task1, task2))
+    }
+
+    @Test
+    fun updateHotseatItems_inDesktopMode_multiInstanceHotseatPackage_shownItems_maintainsOrder() {
+        setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task1, task2),
+            recentTaskPackages = emptyList()
+        )
+        updateRecentTasks( // Trigger a recent-tasks change before calling updateHotseatItems()
+            runningTasks = listOf(task2, task1),
+            recentTaskPackages = emptyList()
+        )
+
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task2, task1),
+            recentTaskPackages = emptyList()
+        )
+
+        val newHotseatItems = recentAppsController.shownHotseatItems
+        assertThat(newHotseatItems).hasSize(1)
+        assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+        assertThat((newHotseatItems[0] as TaskItemInfo).taskId).isEqualTo(1)
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).isEqualTo(listOf(task2))
     }
 
     @Test
@@ -311,12 +413,12 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -327,15 +429,17 @@
     @Test
     fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() {
         setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages =
-                listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_3),
+            runningTasks = listOf(task2, task1, task3),
             recentTaskPackages = emptyList()
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -349,12 +453,12 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2)
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -365,15 +469,17 @@
     @Test
     fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() {
         setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages =
-                listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3),
+            runningTasks = listOf(task1, task2, task3),
             recentTaskPackages = emptyList()
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task2, task1),
             recentTaskPackages = emptyList()
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -385,12 +491,12 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -401,27 +507,31 @@
     @Test
     fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() {
         setInDesktopMode(false)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = recentTaskPackages
         )
+
         setInDesktopMode(true)
         recentTasksChangedListener.onRecentTasksChanged()
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+        assertThat(shownPackages).containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
     }
 
     @Test
     fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = recentTaskPackages
         )
         setInDesktopMode(false)
@@ -437,7 +547,7 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -449,9 +559,11 @@
     @Test
     fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() {
         setInDesktopMode(false)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
@@ -467,7 +579,7 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
@@ -483,14 +595,14 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
         // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
@@ -499,16 +611,18 @@
     @Test
     fun onRecentTasksChanged_inDesktopMode_noActualChangeToRunning_commitRunningAppsToUI_notCalled() {
         setInDesktopMode(true)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = emptyList()
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
         // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = emptyList()
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
@@ -517,21 +631,23 @@
     @Test
     fun onRecentTasksChanged_onlyMinimizedChanges_commitRunningAppsToUI_isCalled() {
         setInDesktopMode(true)
-        val runningTasks = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val task1Minimized = createTask(id = 1, RUNNING_APP_PACKAGE_1, isVisible = false)
+        val task2Visible = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task2Minimized = createTask(id = 2, RUNNING_APP_PACKAGE_2, isVisible = false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTasks,
-            minimizedTaskIndices = setOf(0),
+            runningTasks = listOf(task1Minimized, task2Visible),
             recentTaskPackages = emptyList()
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+
         // Call onRecentTasksChanged() again with a new minimized app, verify we update UI.
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTasks,
-            minimizedTaskIndices = setOf(0, 1),
+            runningTasks = listOf(task1Minimized, task2Minimized),
             recentTaskPackages = emptyList()
         )
+
         verify(taskbarViewController, times(2)).commitRunningAppsToUI()
     }
 
@@ -539,36 +655,46 @@
     fun onRecentTasksChanged_hotseatAppStartsRunning_commitRunningAppsToUI_isCalled() {
         setInDesktopMode(true)
         val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+        val originalTasks = listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1))
+        val newTasks =
+            listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1), createTask(id = 2, HOTSEAT_PACKAGE_1))
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = hotseatPackages,
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1),
+            runningTasks = originalTasks,
             recentTaskPackages = emptyList()
         )
         verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+
         // Call onRecentTasksChanged() again with a new running app, verify we update UI.
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = hotseatPackages,
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, HOTSEAT_PACKAGE_1),
+            runningTasks = newTasks,
             recentTaskPackages = emptyList()
         )
+
         verify(taskbarViewController, times(2)).commitRunningAppsToUI()
     }
 
     private fun prepareHotseatAndRunningAndRecentApps(
         hotseatPackages: List<String>,
-        runningTaskPackages: List<String>,
-        minimizedTaskIndices: Set<Int> = emptySet(),
+        runningTasks: List<Task>,
         recentTaskPackages: List<String>,
     ): Array<ItemInfo?> {
         val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
-        val newHotseatItems =
-            recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-        val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices)
+        recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+        updateRecentTasks(runningTasks, recentTaskPackages)
+        return recentAppsController.shownHotseatItems.toTypedArray()
+    }
+
+    private fun updateRecentTasks(
+        runningTasks: List<Task>,
+        recentTaskPackages: List<String>,
+    ) {
         val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
         val allTasks =
             ArrayList<GroupTask>().apply {
-                if (runningTasks != null) {
-                    add(runningTasks)
+                if (!runningTasks.isEmpty()) {
+                    add(DesktopTask(ArrayList(runningTasks)))
                 }
                 addAll(recentTasks)
             }
@@ -580,20 +706,21 @@
             .whenever(mockRecentsModel)
             .getTasks(any<Consumer<List<GroupTask>>>())
         recentTasksChangedListener.onRecentTasksChanged()
-        return newHotseatItems
     }
 
     private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
-        return packageNames.map {
-            createTestAppInfo(packageName = it).apply {
-                container =
-                    if (it.startsWith("predicted")) {
-                        CONTAINER_HOTSEAT_PREDICTION
-                    } else {
-                        CONTAINER_HOTSEAT
-                    }
+        return packageNames
+            .map {
+                createTestAppInfo(packageName = it).apply {
+                    container =
+                        if (it.startsWith("predicted")) {
+                            CONTAINER_HOTSEAT_PREDICTION
+                        } else {
+                            CONTAINER_HOTSEAT
+                        }
+                }
             }
-        }
+            .map { it.makeWorkspaceItem(taskbarActivityContext) }
     }
 
     private fun createTestAppInfo(
@@ -601,39 +728,24 @@
         className: String = "testClassName"
     ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
 
-    private fun createDesktopTask(
-        packageNames: List<String>,
-        minimizedTaskIndices: Set<Int>
-    ): DesktopTask? {
-        if (packageNames.isEmpty()) return null
-
-        return DesktopTask(
-            ArrayList(
-                packageNames.mapIndexed { index, packageName ->
-                    createTask(packageName, index !in minimizedTaskIndices)
-                }
-            )
-        )
-    }
-
     private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
-        return packageNames.map {
-            if (it.startsWith("split")) {
-                val splitPackages = it.split("_")
+        return packageNames.map { packageName ->
+            if (packageName.startsWith("split")) {
+                val splitPackages = packageName.split("_")
                 GroupTask(
-                    createTask(splitPackages[0]),
-                    createTask(splitPackages[1]),
+                    createTask(100, splitPackages[0]),
+                    createTask(101, splitPackages[1]),
                     /* splitBounds = */ null
                 )
             } else {
-                GroupTask(createTask(it))
+                // Use the number at the end of the test packageName as the id.
+                val id = 1000 + packageName[packageName.length - 1].code
+                GroupTask(createTask(id, packageName))
             }
         }
     }
 
-    private fun createTask(packageName: String, isVisible: Boolean = true): Task {
-        // Use the number at the end of the test packageName as the id.
-        val id = packageName[packageName.length - 1].code
+    private fun createTask(id: Int, packageName: String, isVisible: Boolean = true): Task {
         return Task(
                 Task.TaskKey(
                     id,
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index bb2b7bd..ce5eed9 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -48,20 +48,23 @@
             android:textSize="24sp" />
 
         <TextView
-            android:id="@+id/no_widgets_text"
-            style="@style/PrimaryHeadline"
+            android:id="@+id/widget_picker_description"
             android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:gravity="center"
-            android:textSize="18sp"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:layout_below="@id/title"
+            android:maxLines="1"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:textColor="?attr/widgetPickerDescriptionColor"
             android:visibility="gone"
-            tools:text="@string/no_widgets_available" />
+            android:lineHeight="20sp"
+            android:textSize="14sp" />
 
         <LinearLayout
             android:id="@+id/linear_layout_container"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_below="@id/title">
+            android:layout_below="@id/widget_picker_description">
 
             <FrameLayout
                 android:id="@+id/recycler_view_container"
@@ -124,6 +127,16 @@
                             android:background="@drawable/widgets_surface_background"
                             android:importantForAccessibility="yes"
                             android:id="@+id/right_pane">
+                            <TextView
+                                android:id="@+id/no_widgets_text"
+                                style="@style/PrimaryHeadline"
+                                android:layout_width="match_parent"
+                                android:layout_height="match_parent"
+                                android:gravity="center"
+                                android:textSize="18sp"
+                                android:visibility="gone"
+                                tools:text="@string/no_widgets_available" />
+
                             <!-- Shown when there are recommendations to display -->
                             <LinearLayout
                                 android:id="@+id/widget_recommendations_container"
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index d23f4d1..2688b83 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -26,6 +26,8 @@
     <color name="home_settings_track_off_color">@android:color/system_neutral1_700</color>
 
     <color name="widget_picker_title_color_dark">@android:color/system_neutral1_100</color>
+    <color name="widget_picker_description_color_dark">
+        @android:color/system_neutral2_200</color>
     <color name="widget_picker_header_app_title_color_dark">
         @android:color/system_neutral1_100</color>
     <color name="widget_picker_header_app_subtitle_color_dark">
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index fa87221..7270366 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -75,6 +75,8 @@
 
     <color name="widget_picker_title_color_light">
         @android:color/system_neutral1_900</color>
+    <color name="widget_picker_description_color_light">
+        @android:color/system_neutral2_700</color>
     <color name="widget_picker_header_app_title_color_light">
         @android:color/system_neutral1_900</color>
     <color name="widget_picker_header_app_subtitle_color_light">
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index be8b2e1..e4e047e 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -61,6 +61,7 @@
     <attr name="preloadIconAccentColor" format="color" />
     <attr name="preloadIconBackgroundColor" format="color" />
     <attr name="widgetPickerTitleColor" format="color"/>
+    <attr name="widgetPickerDescriptionColor" format="color"/>
     <attr name="widgetPickerPrimarySurfaceColor" format="color"/>
     <attr name="widgetPickerSecondarySurfaceColor" format="color"/>
     <attr name="widgetPickerHeaderAppTitleColor" format="color"/>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ce80964..8fa1992 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -104,6 +104,7 @@
     <color name="widget_picker_primary_surface_color_light">#EFEDED</color>
     <color name="widget_picker_secondary_surface_color_light">#FAF9F8</color>
     <color name="widget_picker_title_color_light">#1F1F1F</color>
+    <color name="widget_picker_description_color_light">#4C4D50</color>
     <color name="widget_picker_header_app_title_color_light">#1F1F1F</color>
     <color name="widget_picker_header_app_subtitle_color_light">#444746</color>
     <color name="widget_picker_header_background_color_light">#C2E7FF</color>
@@ -123,6 +124,7 @@
     <color name="widget_picker_primary_surface_color_dark">#1F2020</color>
     <color name="widget_picker_secondary_surface_color_dark">#393939</color>
     <color name="widget_picker_title_color_dark">#E3E3E3</color>
+    <color name="widget_picker_description_color_dark">#CCCDCF</color>
     <color name="widget_picker_header_app_title_color_dark">#E3E3E3</color>
     <color name="widget_picker_header_app_subtitle_color_dark">#C4C7C5</color>
     <color name="widget_picker_header_background_color_dark">#004A77</color>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ae3d3b3..f7273a0 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -243,6 +243,7 @@
         <item name="widgetPickerSecondarySurfaceColor">
             @color/widget_picker_secondary_surface_color_light</item>
         <item name="widgetPickerTitleColor">@color/widget_picker_title_color_light</item>
+        <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_light</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_light</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
@@ -282,6 +283,7 @@
             @color/widget_picker_secondary_surface_color_dark</item>
         <item name="widgetPickerTitleColor">
             @color/widget_picker_title_color_dark</item>
+        <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_dark</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_dark</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
diff --git a/src/com/android/launcher3/model/data/TaskItemInfo.kt b/src/com/android/launcher3/model/data/TaskItemInfo.kt
new file mode 100644
index 0000000..fc1cd4d
--- /dev/null
+++ b/src/com/android/launcher3/model/data/TaskItemInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.model.data
+
+/**
+ * Temporary class holding a Task ID to allow us to reference a Task when clicking a hotseat item.
+ *
+ * TODO(b/315344726): Remove this class when we have proper Taskbar support for multi-instance apps
+ */
+class TaskItemInfo(val taskId: Int, itemInfo: WorkspaceItemInfo) : WorkspaceItemInfo(itemInfo)
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index fd15677..2e36583 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -679,6 +679,18 @@
         return sheet;
     }
 
+    /**
+     * Updates the widget picker's title and description in the header to the provided values (if
+     * present).
+     */
+    public void mayUpdateTitleAndDescription(@Nullable String title,
+            @Nullable String descriptionRes) {
+        if (title != null) {
+            mHeaderTitle.setText(title);
+        }
+        // Full sheet doesn't support a description.
+    }
+
     @Override
     public void saveHierarchyState(SparseArray<Parcelable> sparseArray) {
         Bundle bundle = new Bundle();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 840d98a..5b7bbc2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -36,6 +36,7 @@
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -60,9 +61,6 @@
  * Popup for showing the full list of available widgets with a two-pane layout.
  */
 public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
-
-    private static final int PERSONAL_TAB = 0;
-    private static final int WORK_TAB = 1;
     private static final int MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 268;
     private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
     private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
@@ -83,6 +81,7 @@
     private int mActivePage = -1;
     @Nullable
     private PackageUserKey mSelectedHeader;
+    private TextView mHeaderDescription;
 
     public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
@@ -126,6 +125,8 @@
                 newPage -> mRecommendationsCurrentPage = newPage);
 
         mHeaderTitle = mContent.findViewById(R.id.title);
+        mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
+
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
         mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
@@ -141,6 +142,17 @@
     }
 
     @Override
+    public void mayUpdateTitleAndDescription(@Nullable String title, @Nullable String description) {
+        if (title != null) {
+            mHeaderTitle.setText(title);
+        }
+        if (description != null) {
+            mHeaderDescription.setText(description);
+            mHeaderDescription.setVisibility(VISIBLE);
+        }
+    }
+
+    @Override
     protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
         if (enableCategorizedWidgetSuggestions()) {
             // two pane picker is full width for fold as well as tablet.
@@ -371,9 +383,10 @@
     protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
         // The first item is always an empty space entry. Look for any more items.
         boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
-
-        mRightPane.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
-
+        if (!isWidgetAvailable) {
+            mRightPane.removeAllViews();
+            mRightPane.addView(mNoWidgetsView);
+        }
         super.updateRecyclerViewVisibility(adapterHolder);
     }
 
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
index 5dee322..a148744 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
@@ -40,6 +40,7 @@
 import com.android.launcher3.tapl.HomeAppIconMenuItem;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.Test;
@@ -115,6 +116,7 @@
 
     @Test
     @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/350557998
+    @ScreenRecordRule.ScreenRecord // b/350557998
     public void testShortcutIconWithTheme() throws Exception {
         setThemeEnabled(true);
         initialize(this);
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index bc26c00..ae24a57 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -352,11 +352,8 @@
     }
 
     private void assertPagesExist(Launcher launcher, int... pageIds) {
-        waitForLauncherCondition("Existing page count does NOT match. "
-                + "Expected: " + pageIds.length
-                + ". Actual: " + launcher.getWorkspace().getPageCount(),
-                l -> pageIds.length == l.getWorkspace().getPageCount());
         int pageCount = launcher.getWorkspace().getPageCount();
+        assertEquals("Existing page count does NOT match.", pageIds.length, pageCount);
         for (int i = 0; i < pageCount; i++) {
             CellLayout page = (CellLayout) launcher.getWorkspace().getPageAt(i);
             int pageId = launcher.getWorkspace().getCellLayoutId(page);