Merge "Avoid per task app icons in Taskbar for desktop mode" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 2745129..6db6233 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -85,6 +85,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
@@ -1342,7 +1343,7 @@
                 mControllers.uiController.onTaskbarIconLaunched(api);
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
-        } else if (tag instanceof TaskItemInfo info) {
+        } else if (tag instanceof TaskItemInfo info && !Flags.enableMultiInstanceMenuTaskbar()) {
             RemoteTransition remoteTransition = canUnminimizeDesktopTask(info.getTaskId())
                     ? createUnminimizeRemoteTransition() : null;
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index a059b22..e654fe8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -19,6 +19,7 @@
 import android.window.DesktopModeFlags
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.BubbleTextView.RunningAppState
+import com.android.launcher3.Flags
 import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.TaskItemInfo
@@ -276,27 +277,60 @@
         return newOrder
     }
 
+    /**
+     * Computes the list of running tasks to be shown in the recent apps section of the taskbar in
+     * desktop mode, taking into account deduplication against hotseat items and existing tasks.
+     */
     private fun computeShownRunningTasks(): List<GroupTask> {
         if (!canShowRunningApps) {
             return emptyList()
         }
-        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 desktopTasks = getOrderedAndWrappedDesktopTasks()
+
         val newShownTasks =
-            shownTasks
-                .filter { !it.supportsMultipleTasks() }
-                .filter { it.task1.key.id in desktopTaskIds }
-                .toMutableList()
-        // Add any new Tasks, maintaining the order from previous shownTasks.
-        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 }
+            if (Flags.enableMultiInstanceMenuTaskbar()) {
+                val deduplicatedDesktopTasks =
+                    desktopTasks.distinctBy { Pair(it.task1.key.packageName, it.task1.key.userId) }
+
+                shownTasks
+                    .filter {
+                        !it.supportsMultipleTasks() &&
+                            it.task1.key.id in deduplicatedDesktopTasks.map { it.task1.key.id }
+                    }
+                    .toMutableList()
+                    .apply {
+                        addAll(
+                            deduplicatedDesktopTasks.filter { currentTask ->
+                                val currentTaskKey = currentTask.task1.key
+                                currentTaskKey.id !in shownTasks.map { it.task1.key.id } &&
+                                    shownHotseatItems.none { hotseatItem ->
+                                        hotseatItem.targetPackage == currentTaskKey.packageName &&
+                                            hotseatItem.user.identifier == currentTaskKey.userId
+                                    }
+                            }
+                        )
+                    }
+            } else {
+                val desktopTaskIds = desktopTasks.map { it.task1.key.id }
+                val shownHotseatItemTaskIds =
+                    shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId }
+
+                shownTasks
+                    .filter { !it.supportsMultipleTasks() && it.task1.key.id in desktopTaskIds }
+                    .toMutableList()
+                    .apply {
+                        addAll(
+                            desktopTasks.filter { desktopTask ->
+                                desktopTask.task1.key.id !in
+                                    shownTasks.map { shownTask -> shownTask.task1.key.id }
+                            }
+                        )
+                        removeAll { it.task1.key.id in shownHotseatItemTaskIds }
+                    }
+            }
+
+        return newShownTasks
     }
 
     private fun computeShownRecentTasks(): List<GroupTask> {
@@ -305,7 +339,6 @@
         }
         // Remove the current task.
         val allRecentTasks = allRecentTasks.subList(0, allRecentTasks.size - 1)
-        // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too
         var shownTasks = dedupeHotseatTasks(allRecentTasks, shownHotseatItems)
         if (shownTasks.size > MAX_RECENT_TASKS) {
             // Remove any tasks older than MAX_RECENT_TASKS.
@@ -318,10 +351,22 @@
         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)
+        return if (Flags.enableMultiInstanceMenuTaskbar()) {
+            groupTasks.filter { groupTask ->
+                val taskKey = groupTask.task1.key
+                // Keep tasks that are group tasks or unique package name/user combinations
+                groupTask.hasMultipleTasks() ||
+                    shownHotseatItems.none {
+                        it.targetPackage == taskKey.packageName &&
+                            it.user.identifier == taskKey.userId
+                    }
+            }
+        } else {
+            val hotseatPackages = shownHotseatItems.map { it.targetPackage }
+            groupTasks.filter { groupTask ->
+                groupTask.hasMultipleTasks() ||
+                    !hotseatPackages.contains(groupTask.task1.key.packageName)
+            }
         }
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
index 13880f1..9ca8a1b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -19,9 +19,11 @@
 import android.animation.AnimatorTestRule
 import android.content.ComponentName
 import android.content.Intent
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR
 import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
@@ -64,6 +66,7 @@
     FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
     FLAG_ENABLE_BUBBLE_BAR,
 )
+@DisableFlags(FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR)
 class TaskbarOverflowTest {
     @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
 
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index ed0c928..fbfc40b 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -23,10 +23,12 @@
 import android.content.res.Resources
 import android.os.Process
 import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.rule.TestWatcher
 import android.testing.AndroidTestingRunner
 import com.android.internal.R
 import com.android.launcher3.BubbleTextView.RunningAppState
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
 import com.android.launcher3.model.data.AppInfo
@@ -57,6 +59,7 @@
 import org.mockito.kotlin.whenever
 
 @RunWith(AndroidTestingRunner::class)
+@EnableFlags(Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR)
 class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
 
     @get:Rule val mockitoRule = MockitoJUnit.rule()
@@ -323,8 +326,12 @@
         assertThat(hotseatItem1.taskId).isEqualTo(1)
     }
 
+    /**
+     * Tests that in desktop mode, when two tasks have the same package name and one is in the
+     * hotseat, only the hotseat item represents the app, and no duplicate is shown in recent apps.
+     */
     @Test
-    fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_hotseatCoversFirstTask() {
+    fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_onlyHotseatCoversTask() {
         setInDesktopMode(true)
 
         val newHotseatItems =
@@ -338,16 +345,16 @@
                 recentTaskPackages = emptyList(),
             )
 
-        // First task is in Hotseat Items
+        // The 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
+        assertThat(hotseatItem1.targetPackage).isEqualTo(HOTSEAT_PACKAGE_1)
+
+        // The other task of the same package is not in shownTasks
         val shownTasks = recentAppsController.shownTasks.map { it.task1 }
-        assertThat(shownTasks)
-            .containsExactlyElementsIn(listOf(createTask(id = 2, HOTSEAT_PACKAGE_1)))
+        assertThat(shownTasks).isEmpty()
     }
 
     @Test
@@ -530,8 +537,12 @@
         assertThat(shownTasks).isEqualTo(listOf(task1, task2))
     }
 
+    /**
+     * Tests that when multiple instances of the same app are running in desktop mode and the app is
+     * not in the hotseat, only one instance is shown in the recent apps section.
+     */
     @Test
-    fun onRecentTasksChanged_inDesktopMode_multiInstance_shownTasks_maintainsOrder() {
+    fun onRecentTasksChanged_inDesktopMode_multiInstance_noHotseat_shownTasksHasOneInstance() {
         setInDesktopMode(true)
         val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
         val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1)
@@ -541,43 +552,10 @@
             recentTaskPackages = emptyList(),
         )
 
-        prepareHotseatAndRunningAndRecentApps(
-            hotseatPackages = emptyList(),
-            runningTasks = listOf(task2, task1),
-            recentTaskPackages = emptyList(),
-        )
-
+        // Assert that shownTasks contains only one instance of the app
         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))
+        assertThat(shownTasks).hasSize(1)
+        assertThat(shownTasks[0].key.packageName).isEqualTo(RUNNING_APP_PACKAGE_1)
     }
 
     @Test