Merge changes from topic "taskbar-running-minimized" into main

* changes:
  Maintain Running Tasks order in Desktop mode
  Change running apps section to use GroupTask instead of AppInfo
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 2b0e169..365c5c4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -15,9 +15,6 @@
  */
 package com.android.launcher3.taskbar;
 
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
-
 import android.util.SparseArray;
 import android.view.View;
 
@@ -29,7 +26,6 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -37,8 +33,6 @@
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.RecentsModel;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -54,7 +48,7 @@
  * Launcher model Callbacks for rendering taskbar.
  */
 public class TaskbarModelCallbacks implements
-        BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener {
+        BgDataModel.Callbacks, LauncherBindableItemsContainer {
 
     private final SparseArray<ItemInfo> mHotseatItems = new SparseArray<>();
     private List<ItemInfo> mPredictedItems = Collections.emptyList();
@@ -68,8 +62,6 @@
     // Used to defer any UI updates during the SUW unstash animation.
     private boolean mDeferUpdatesForSUW;
     private Runnable mDeferredUpdates;
-    private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener =
-            visible -> updateRunningApps();
 
     public TaskbarModelCallbacks(
             TaskbarActivityContext context, TaskbarView container) {
@@ -79,39 +71,6 @@
 
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
-        if (mControllers.taskbarRecentAppsController.getCanShowRunningApps()) {
-            RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this);
-
-            if (shouldShowRunningAppsInDesktopMode()) {
-                DesktopVisibilityController desktopVisibilityController =
-                        LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-                if (desktopVisibilityController != null) {
-                    desktopVisibilityController.registerDesktopVisibilityListener(
-                            mDesktopVisibilityListener);
-                }
-            }
-        }
-    }
-
-    /**
-     * Unregisters listeners in this class.
-     */
-    public void unregisterListeners() {
-        RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener();
-
-        if (shouldShowRunningAppsInDesktopMode()) {
-            DesktopVisibilityController desktopVisibilityController =
-                    LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-            if (desktopVisibilityController != null) {
-                desktopVisibilityController.unregisterDesktopVisibilityListener(
-                        mDesktopVisibilityListener);
-            }
-        }
-    }
-
-    private boolean shouldShowRunningAppsInDesktopMode() {
-        // TODO(b/335401172): unify DesktopMode checks in Launcher
-        return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
     }
 
     @Override
@@ -232,10 +191,12 @@
                 predictionNextIndex++;
             }
         }
-        hotseatItemInfos = mControllers.taskbarRecentAppsController
-                .updateHotseatItemInfos(hotseatItemInfos);
-        Set<String> runningPackages = mControllers.taskbarRecentAppsController.getRunningApps();
-        Set<String> minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps();
+
+        final TaskbarRecentAppsController recentAppsController =
+                mControllers.taskbarRecentAppsController;
+        hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
+        Set<String> runningPackages = recentAppsController.getRunningAppPackages();
+        Set<String> minimizedPackages = recentAppsController.getMinimizedAppPackages();
 
         if (mDeferUpdatesForSUW) {
             ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
@@ -270,21 +231,11 @@
         }
     }
 
-    @Override
-    public void onRunningTasksChanged() {
-        updateRunningApps();
-    }
-
     /** Called when there's a change in running apps to update the UI. */
     public void commitRunningAppsToUI() {
         commitItemsToUI();
     }
 
-    /** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */
-    public void updateRunningApps() {
-        mControllers.taskbarRecentAppsController.updateRunningApps();
-    }
-
     @Override
     public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
         mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy);
@@ -296,7 +247,6 @@
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         Preconditions.assertUIThread();
         mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap);
-        mControllers.taskbarRecentAppsController.setApps(apps);
     }
 
     protected void dumpLogs(String prefix, PrintWriter pw) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index b1fc9cc..659f7c8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,16 +15,13 @@
  */
 package com.android.launcher3.taskbar
 
-import android.app.ActivityManager.RunningTaskInfo
-import android.app.WindowConfiguration
 import androidx.annotation.VisibleForTesting
-import com.android.launcher3.Flags.enableRecentsInTaskbar
-import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
 import com.android.quickstep.RecentsModel
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
 import com.android.window.flags.Flags.enableDesktopWindowingMode
 import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
 import java.io.PrintWriter
@@ -42,22 +39,22 @@
 ) : LoggableTaskbarController {
 
     // TODO(b/335401172): unify DesktopMode checks in Launcher.
-    val canShowRunningApps =
+    var canShowRunningApps =
         enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
-
-    // TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
-    var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps
         @VisibleForTesting
-        set(isEnabledFromTest){
+        set(isEnabledFromTest) {
             field = isEnabledFromTest
         }
 
     // Initialized in init.
     private lateinit var controllers: TaskbarControllers
 
-    private var apps: Array<AppInfo>? = null
-    private var allRunningDesktopAppInfos: List<AppInfo>? = null
-    private var allMinimizedDesktopAppInfos: List<AppInfo>? = null
+    private var shownHotseatItems: List<ItemInfo> = emptyList()
+    private var allRecentTasks: List<GroupTask> = emptyList()
+    private var desktopTask: DesktopTask? = null
+    // TODO(next CL): actually read and show these
+    var shownTasks: List<GroupTask> = emptyList()
+        private set
 
     private val desktopVisibilityController: DesktopVisibilityController?
         get() = desktopVisibilityControllerProvider()
@@ -65,122 +62,139 @@
     private val isInDesktopMode: Boolean
         get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
 
-    val runningApps: Set<String>
+    val runningAppPackages: Set<String>
+        /**
+         * Returns the package names 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() {
-            if (!isEnabled || !isInDesktopMode) {
+            if (!canShowRunningApps || !isInDesktopMode) {
                 return emptySet()
             }
-            return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet()
+            val tasks = desktopTask?.tasks ?: return emptySet()
+            return tasks.map { task -> task.key.packageName }.toSet()
         }
 
-    val minimizedApps: Set<String>
+    val minimizedAppPackages: Set<String>
+        /**
+         * 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).
+         */
         get() {
-            if (!isInDesktopMode) {
+            if (!canShowRunningApps || !isInDesktopMode) {
                 return emptySet()
             }
-            return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet()
-                ?: emptySet()
+            val desktopTasks = desktopTask?.tasks ?: return emptySet()
+            val packageToTasks = desktopTasks.groupBy { it.key.packageName }
+            return packageToTasks.filterValues { tasks -> tasks.all { !it.isVisible } }.keys
         }
 
+    private val recentTasksChangedListener =
+        RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() }
+
+    // TODO(b/343291428): add TaskVisualsChangListener as well (for calendar/clock?)
+
+    // Used to keep track of the last requested task list ID, so that we do not request to load the
+    // tasks again if we have already requested it and the task list has not changed
+    private var taskListChangeId = -1
+
     fun init(taskbarControllers: TaskbarControllers) {
         controllers = taskbarControllers
+        recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
+        reloadRecentTasksIfNeeded()
     }
 
     fun onDestroy() {
-        apps = null
-    }
-
-    /** Stores the current [AppInfo] instances, no-op except in desktop environment. */
-    fun setApps(apps: Array<AppInfo>?) {
-        this.apps = apps
+        recentsModel.unregisterRecentTasksChangedListener()
     }
 
     /** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */
-    // TODO(next CL): add new section of Tasks instead of changing Hotseat items
     fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo?>): Array<ItemInfo?> {
-        if (!isEnabled || !isInDesktopMode) {
+        // Ignore predicted apps - we show running or recent apps instead.
+        val removePredictions = isInDesktopMode && canShowRunningApps
+        if (!removePredictions) {
+            shownHotseatItems = hotseatItems.filterNotNull()
+            onRecentsOrHotseatChanged()
             return hotseatItems
         }
-        val newHotseatItemInfos =
+        shownHotseatItems =
             hotseatItems
                 .filterNotNull()
-                // Ignore predicted apps - we show running apps instead
                 .filter { itemInfo -> !itemInfo.isPredictedItem }
                 .toMutableList()
-        val runningDesktopAppInfos =
-            allRunningDesktopAppInfos?.let {
-                getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList())
+
+        onRecentsOrHotseatChanged()
+
+        return shownHotseatItems.toTypedArray()
+    }
+
+    private fun reloadRecentTasksIfNeeded() {
+        if (!recentsModel.isTaskListValid(taskListChangeId)) {
+            taskListChangeId =
+                recentsModel.getTasks { tasks ->
+                    allRecentTasks = tasks
+                    desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
+                    onRecentsOrHotseatChanged()
+                    controllers.taskbarViewController.commitRunningAppsToUI()
+                }
+        }
+    }
+
+    private fun onRecentsOrHotseatChanged() {
+        shownTasks =
+            if (isInDesktopMode) {
+                computeShownRunningTasks()
+            } else {
+                computeShownRecentTasks()
             }
-        if (runningDesktopAppInfos != null) {
-            newHotseatItemInfos.addAll(runningDesktopAppInfos)
-        }
-        return newHotseatItemInfos.toTypedArray()
     }
 
-    private fun getRunningDesktopAppInfosExceptHotseatApps(
-        allRunningDesktopAppInfos: List<AppInfo>,
-        hotseatItems: List<ItemInfo>
-    ): List<ItemInfo> {
-        val hotseatPackages = hotseatItems.map { it.targetPackage }
-        return allRunningDesktopAppInfos
-            .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) }
-            .map { WorkspaceItemInfo(it) }
-    }
-
-    private fun getDesktopRunningTasks(): List<RunningTaskInfo> =
-        recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo ->
-            taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM
-        }
-
-    // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos
-    private fun getAppInfosFromRunningTasks(tasks: List<RunningTaskInfo>): List<AppInfo> {
-        // Early return if apps is empty, since we then have no AppInfo to compare to
-        if (apps == null) {
+    private fun computeShownRunningTasks(): List<GroupTask> {
+        if (!canShowRunningApps) {
             return emptyList()
         }
-        val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull()
-        return packageNames
-            .map { packageName -> apps?.find { app -> packageName == app.targetPackage } }
-            .filterNotNull()
+        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 }
+        // Add any new Tasks, maintaining the order from previous shownTasks.
+        newShownTasks.addAll(desktopTaskAsList.filter { it.packageNames !in newShownPackages })
+        return newShownTasks.toList()
     }
 
-    /** Called to update the list of currently running apps, no-op except in desktop environment. */
-    fun updateRunningApps() {
-        if (!isEnabled || !isInDesktopMode) {
-            return controllers.taskbarViewController.commitRunningAppsToUI()
+    private fun computeShownRecentTasks(): List<GroupTask> {
+        // TODO(next CL): implement Recents section
+        return emptyList()
+    }
+
+    private fun dedupeHotseatTasks(
+        groupTasks: List<GroupTask>,
+        shownHotseatItems: List<ItemInfo>
+    ): List<GroupTask> {
+        val hotseatPackages = shownHotseatItems.map { item -> item.targetPackage }
+        return groupTasks.filter { groupTask ->
+            groupTask.hasMultipleTasks() ||
+                !hotseatPackages.contains(groupTask.task1.key.packageName)
         }
-        val runningTasks = getDesktopRunningTasks()
-        val runningAppInfo = getAppInfosFromRunningTasks(runningTasks)
-        allRunningDesktopAppInfos = runningAppInfo
-        updateMinimizedApps(runningTasks, runningAppInfo)
-        controllers.taskbarViewController.commitRunningAppsToUI()
-    }
-
-    private fun updateMinimizedApps(
-        runningTasks: List<RunningTaskInfo>,
-        runningAppInfo: List<AppInfo>,
-    ) {
-        val allRunningAppTasks =
-            runningAppInfo
-                .mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } }
-                .associate { (appInfo, targetPackage) ->
-                    appInfo to
-                            runningTasks
-                                .filter { it.realActivity?.packageName == targetPackage }
-                                .map { it.taskId }
-                }
-        val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible }
-        allMinimizedDesktopAppInfos =
-            allRunningAppTasks
-                .filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } }
-                .keys
-                .toList()
     }
 
     override fun dumpLogs(prefix: String, pw: PrintWriter) {
         pw.println("$prefix TaskbarRecentAppsController:")
-        pw.println("$prefix\tisEnabled=$isEnabled")
         pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
-        // TODO(next CL): add more logs
+        pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}")
+        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")
     }
+
+    private val GroupTask.packageNames: List<String>
+        get() = tasks.map { task -> task.key.packageName }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 55745b5..b03894de 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -224,7 +224,6 @@
         }
         LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks);
         mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
-        mModelCallbacks.unregisterListeners();
     }
 
     public boolean areIconsVisible() {
@@ -869,6 +868,11 @@
         return mTaskbarView.isEventOverAnyItem(ev);
     }
 
+    /** Called when there's a change in running apps to update the UI. */
+    public void commitRunningAppsToUI() {
+        mModelCallbacks.commitRunningAppsToUI();
+    }
+
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarViewController:");
@@ -889,14 +893,4 @@
         mModelCallbacks.dumpLogs(prefix + "\t", pw);
     }
 
-    /** Called when there's a change in running apps to update the UI. */
-    public void commitRunningAppsToUI() {
-        mModelCallbacks.commitRunningAppsToUI();
-    }
-
-    /** Call TaskbarModelCallbacks to update running apps. */
-    public void updateRunningApps() {
-        mModelCallbacks.updateRunningApps();
-    }
-
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
index 668a87d..ac7dd06 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
@@ -33,7 +33,7 @@
     val hasNavButtons = taskbarActivityContext.isThreeButtonNav
 
     val hasRecents: Boolean
-        get() = taskbarControllers.taskbarRecentAppsController.isEnabled
+        get() = taskbarControllers.taskbarRecentAppsController.shownTasks.isNotEmpty()
 
     val hasDivider: Boolean
         get() = enableTaskbarPinning() || hasRecents
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index b08a46f..66091d4 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -70,7 +70,8 @@
     private TaskLoadResult mResultsBg = INVALID_RESULT;
     private TaskLoadResult mResultsUi = INVALID_RESULT;
 
-    private RecentsModel.RunningTasksListener mRunningTasksListener;
+    private @Nullable RecentsModel.RunningTasksListener mRunningTasksListener;
+    private @Nullable RecentsModel.RecentTasksChangedListener mRecentTasksChangedListener;
     // Tasks are stored in order of least recently launched to most recently launched.
     private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
 
@@ -199,6 +200,9 @@
 
     public void onRecentTasksChanged() {
         invalidateLoadedTasks();
+        if (mRecentTasksChangedListener != null) {
+            mRecentTasksChangedListener.onRecentTasksChanged();
+        }
     }
 
     private synchronized void invalidateLoadedTasks() {
@@ -221,6 +225,21 @@
         mRunningTasksListener = null;
     }
 
+    /**
+     * Registers a listener for running tasks
+     */
+    public void registerRecentTasksChangedListener(
+            RecentsModel.RecentTasksChangedListener listener) {
+        mRecentTasksChangedListener = listener;
+    }
+
+    /**
+     * Removes the previously registered running tasks listener
+     */
+    public void unregisterRecentTasksChangedListener() {
+        mRecentTasksChangedListener = null;
+    }
+
     private void initRunningTasks(ArrayList<ActivityManager.RunningTaskInfo> runningTasks) {
         // Tasks are retrieved in order of most recently launched/used to least recently launched.
         mRunningTasks = new ArrayList<>(runningTasks);
@@ -357,6 +376,7 @@
             task.setLastSnapshotData(taskInfo);
             task.positionInParent = taskInfo.positionInParent;
             task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds();
+            task.isVisible = taskInfo.isVisible;
             tasks.add(task);
         }
         return new DesktopTask(tasks);
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 6eefe4a..b796951 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -42,6 +42,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.recents.data.RecentTasksDataSource;
+import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -301,6 +302,8 @@
 
     /**
      * Registers a listener for running tasks
+     * TODO(b/343292503): Should we remove RunningTasksListener entirely if it's not needed?
+     *  (Note that Desktop mode gets the running tasks by checking {@link DesktopTask#tasks}
      */
     public void registerRunningTasksListener(RunningTasksListener listener) {
         mTaskList.registerRunningTasksListener(listener);
@@ -314,6 +317,20 @@
     }
 
     /**
+     * Registers a listener for recent tasks
+     */
+    public void registerRecentTasksChangedListener(RecentTasksChangedListener listener) {
+        mTaskList.registerRecentTasksChangedListener(listener);
+    }
+
+    /**
+     * Removes the previously registered running tasks listener
+     */
+    public void unregisterRecentTasksChangedListener() {
+        mTaskList.unregisterRecentTasksChangedListener();
+    }
+
+    /**
      * Gets the set of running tasks.
      */
     public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks() {
@@ -379,4 +396,14 @@
          */
         void onRunningTasksChanged();
     }
+
+    /**
+     * Listener for receiving recent tasks changes
+     */
+    public interface RecentTasksChangedListener {
+        /**
+         * Called when there's a change to recent tasks
+         */
+        void onRecentTasksChanged();
+    }
 }
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 104263a..5813708 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -16,24 +16,34 @@
 
 package com.android.launcher3.taskbar
 
-import android.app.ActivityManager.RunningTaskInfo
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.ComponentName
 import android.content.Intent
 import android.os.Process
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+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.statehandlers.DesktopVisibilityController
 import com.android.quickstep.RecentsModel
+import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
 import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
 @RunWith(AndroidTestingRunner::class)
@@ -45,173 +55,292 @@
     @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
 
     private var nextTaskId: Int = 500
+    private var taskListChangeId: Int = 1
 
     private lateinit var recentAppsController: TaskbarRecentAppsController
+    private lateinit var recentTasksChangedListener: RecentTasksChangedListener
     private lateinit var userHandle: UserHandle
 
     @Before
     fun setUp() {
         super.setup()
         userHandle = Process.myUserHandle()
+
         recentAppsController =
             TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
         recentAppsController.init(taskbarControllers)
-        recentAppsController.isEnabled = true
-        recentAppsController.setApps(
-            ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
-        )
+        recentAppsController.canShowRunningApps = true
+
+        val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
+        verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
+        recentTasksChangedListener = listenerCaptor.value
     }
 
     @Test
-    fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() {
-        setInDesktopMode(false)
-        val hotseatItems =
-            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-
-        assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
-            .isEqualTo(hotseatItems.toTypedArray())
-    }
-
-    @Test
-    fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() {
-        setInDesktopMode(false)
-        val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
-        val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
-        val runningTasks =
-            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
-        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        recentAppsController.updateRunningApps()
-
+    fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() {
+        recentAppsController.canShowRunningApps = false
+        setInDesktopMode(true)
+        val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1)
         val newHotseatItems =
-            recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
+            prepareHotseatAndRunningAndRecentApps(
+                hotseatPackages = hotseatPackages,
+                runningTaskPackages = emptyList(),
+                recentTaskPackages = emptyList()
+            )
         assertThat(newHotseatItems.map { it?.targetPackage })
             .containsExactlyElementsIn(hotseatPackages)
     }
 
     @Test
-    fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() {
+    fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() {
+        recentAppsController.canShowRunningApps = true
         setInDesktopMode(true)
-        val hotseatItems =
-            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-
-        assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
-            .isEqualTo(hotseatItems.toTypedArray())
-    }
-
-    @Test
-    fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() {
-        setInDesktopMode(true)
-        val hotseatItems =
-            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-        val runningTasks =
-            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
-        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        recentAppsController.updateRunningApps()
-
         val newHotseatItems =
-            recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
-        val expectedPackages =
-            listOf(
-                HOTSEAT_PACKAGE_1,
-                HOTSEAT_PACKAGE_2,
-                RUNNING_APP_PACKAGE_1,
-                RUNNING_APP_PACKAGE_2,
+            prepareHotseatAndRunningAndRecentApps(
+                hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+                runningTaskPackages = emptyList(),
+                recentTaskPackages = emptyList()
             )
+        val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
         assertThat(newHotseatItems.map { it?.targetPackage })
             .containsExactlyElementsIn(expectedPackages)
     }
 
     @Test
-    fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() {
+    fun onRecentTasksChanged_cantShowRunning_inDesktopMode_shownTasks_returnsEmptyList() {
+        recentAppsController.canShowRunningApps = false
         setInDesktopMode(true)
-        val hotseatItems =
-            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-        val runningTasks =
-            createDesktopTasksFromPackageNames(
-                listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
-            )
-        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        recentAppsController.updateRunningApps()
-
-        val newHotseatItems =
-            recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
-        val expectedPackages =
-            listOf(
-                HOTSEAT_PACKAGE_1,
-                HOTSEAT_PACKAGE_2,
-                RUNNING_APP_PACKAGE_1,
-                RUNNING_APP_PACKAGE_2,
-            )
-        assertThat(newHotseatItems.map { it?.targetPackage })
-            .containsExactlyElementsIn(expectedPackages)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+            runningTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+            recentTaskPackages = emptyList()
+        )
+        assertThat(recentAppsController.shownTasks).isEmpty()
     }
 
     @Test
-    fun getRunningApps_notInDesktopMode_returnsEmptySet() {
+    fun onRecentTasksChanged_inDesktopMode_noRunningApps_shownTasks_returnsEmptyList() {
+        setInDesktopMode(true)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = emptyList(),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        assertThat(recentAppsController.shownTasks).isEmpty()
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() {
+        setInDesktopMode(true)
+        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = runningTaskPackages,
+            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)
+    }
+
+    @Test
+    fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() {
         setInDesktopMode(false)
-        val runningTasks =
-            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
-        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        recentAppsController.updateRunningApps()
-
-        assertThat(recentAppsController.runningApps).isEmpty()
-        assertThat(recentAppsController.minimizedApps).isEmpty()
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            recentTaskPackages = emptyList()
+        )
+        assertThat(recentAppsController.runningAppPackages).isEmpty()
+        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
     }
 
     @Test
-    fun getRunningApps_inDesktopMode_returnsRunningApps() {
+    fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() {
         setInDesktopMode(true)
-        val runningTasks =
-            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
-        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        recentAppsController.updateRunningApps()
+        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = runningTaskPackages,
+            recentTaskPackages = emptyList()
+        )
+        assertThat(recentAppsController.runningAppPackages)
+            .containsExactlyElementsIn(runningTaskPackages)
+        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+    }
 
-        assertThat(recentAppsController.runningApps)
-            .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
-        assertThat(recentAppsController.minimizedApps).isEmpty()
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() {
+        setInDesktopMode(true)
+        val runningTaskPackages =
+            listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+            runningTaskPackages = runningTaskPackages,
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        assertThat(recentAppsController.runningAppPackages)
+            .containsExactlyElementsIn(runningTaskPackages)
+        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
     }
 
     @Test
     fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() {
         setInDesktopMode(true)
-        val runningTasks =
-            ArrayList(
-                listOf(
-                    createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true },
-                    createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true },
-                    createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false },
-                )
-            )
-        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        recentAppsController.updateRunningApps()
+        val runningTaskPackages =
+            listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
+        val minimizedTaskIndices = setOf(2) // RUNNING_APP_PACKAGE_3
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = runningTaskPackages,
+            minimizedTaskIndices = minimizedTaskIndices,
+            recentTaskPackages = emptyList()
+        )
+        assertThat(recentAppsController.runningAppPackages)
+            .containsExactlyElementsIn(runningTaskPackages)
+        assertThat(recentAppsController.minimizedAppPackages).containsExactly(RUNNING_APP_PACKAGE_3)
+    }
 
-        assertThat(recentAppsController.runningApps)
-            .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
-        assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3)
+    @Test
+    fun getMinimizedApps_inDesktopMode_twoTasksSamePackageOneMinimizedReturnsNotMinimized() {
+        setInDesktopMode(true)
+        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_1)
+        val minimizedTaskIndices = setOf(1) // The second RUNNING_APP_PACKAGE_1 task.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = runningTaskPackages,
+            minimizedTaskIndices = minimizedTaskIndices,
+            recentTaskPackages = emptyList()
+        )
+        assertThat(recentAppsController.runningAppPackages)
+            .containsExactlyElementsIn(runningTaskPackages.toSet())
+        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() {
+        setInDesktopMode(true)
+        val originalOrder = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = originalOrder,
+            recentTaskPackages = emptyList()
+        )
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+            recentTaskPackages = emptyList()
+        )
+        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+        assertThat(shownPackages).isEqualTo(originalOrder)
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() {
+        setInDesktopMode(true)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            recentTaskPackages = emptyList()
+        )
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages =
+                listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_3),
+            recentTaskPackages = emptyList()
+        )
+        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+        val expectedOrder =
+            listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
+        assertThat(shownPackages).isEqualTo(expectedOrder)
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() {
+        setInDesktopMode(true)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages =
+                listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3),
+            recentTaskPackages = emptyList()
+        )
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+            recentTaskPackages = emptyList()
+        )
+        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+        assertThat(shownPackages).isEqualTo(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+    }
+
+    @Test
+    fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() {
+        setInDesktopMode(false)
+        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = runningTaskPackages,
+            recentTaskPackages = recentTaskPackages
+        )
+        setInDesktopMode(true)
+        recentTasksChangedListener.onRecentTasksChanged()
+        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+        assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+    }
+
+    private fun prepareHotseatAndRunningAndRecentApps(
+        hotseatPackages: List<String>,
+        runningTaskPackages: List<String>,
+        minimizedTaskIndices: Set<Int> = emptySet(),
+        recentTaskPackages: List<String>,
+    ): Array<ItemInfo?> {
+        val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
+        val newHotseatItems =
+            recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+        val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices)
+        val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
+        val allTasks =
+            ArrayList<GroupTask>().apply {
+                if (runningTasks != null) {
+                    add(runningTasks)
+                }
+                addAll(recentTasks)
+            }
+        doAnswer {
+                val callback: Consumer<ArrayList<GroupTask>> = it.getArgument(0)
+                callback.accept(allTasks)
+                taskListChangeId
+            }
+            .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) }
-    }
-
-    private fun createDesktopTasksFromPackageNames(
-        packageNames: List<String>
-    ): ArrayList<RunningTaskInfo> {
-        return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) })
-    }
-
-    private fun createDesktopTaskInfo(
-        packageName: String,
-        init: RunningTaskInfo.() -> Unit = { isVisible = true },
-    ): RunningTaskInfo {
-        return RunningTaskInfo().apply {
-            taskId = nextTaskId++
-            configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
-            realActivity = ComponentName(packageName, "TestActivity")
-            init()
+        return packageNames.map {
+            createTestAppInfo(packageName = it).apply {
+                container =
+                    if (it.startsWith("predicted")) {
+                        CONTAINER_HOTSEAT_PREDICTION
+                    } else {
+                        CONTAINER_HOTSEAT
+                    }
+            }
         }
     }
 
@@ -220,23 +349,54 @@
         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 { GroupTask(createTask(it)) }
+    }
+
+    private fun createTask(packageName: String, isVisible: Boolean = true): Task {
+        return Task(
+                Task.TaskKey(
+                    nextTaskId++,
+                    WINDOWING_MODE_FREEFORM,
+                    Intent().apply { `package` = packageName },
+                    ComponentName(packageName, "TestActivity"),
+                    userHandle.identifier,
+                    0
+                )
+            )
+            .apply { this.isVisible = isVisible }
+    }
+
     private fun setInDesktopMode(inDesktopMode: Boolean) {
         whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
     }
 
+    private val GroupTask.packageNames: List<String>
+        get() = tasks.map { task -> task.key.packageName }
+
     private companion object {
         const val HOTSEAT_PACKAGE_1 = "hotseat1"
         const val HOTSEAT_PACKAGE_2 = "hotseat2"
+        const val PREDICTED_PACKAGE_1 = "predicted1"
         const val RUNNING_APP_PACKAGE_1 = "running1"
         const val RUNNING_APP_PACKAGE_2 = "running2"
         const val RUNNING_APP_PACKAGE_3 = "running3"
-        val ALL_APP_PACKAGES =
-            listOf(
-                HOTSEAT_PACKAGE_1,
-                HOTSEAT_PACKAGE_2,
-                RUNNING_APP_PACKAGE_1,
-                RUNNING_APP_PACKAGE_2,
-                RUNNING_APP_PACKAGE_3,
-            )
+        const val RECENT_PACKAGE_1 = "recent1"
+        const val RECENT_PACKAGE_2 = "recent2"
     }
 }