TaskRepository performance improvement

This CL improves Overview's performance by preventing multiple thumbnail and icon fetches for visible tasks. It introduces manual cancellation and removal of tasks that are no longer visible to prevent fetching and listening to changes in such tasks.
- Renamed 'augmentedTasks' to 'tasks' and updated it to be a MutableStateFlow with a map of Task Id and Task.
- When a new task becomes visible, the task is added to a list of requests along with a Job that fetches the Thumbnail and Icon. Additionally, 'taskVisualChangesDelegated' is added and removed for that task per request.
- When a task becomes invisible, the Job is canceled to prevent fetching the Thumbnail and Icon. The thumbnail and icon are cleared from the list of tasks, and the listener from 'taskVisualChangesDelegated' is unregistered.
- The list of tasks is updated when the list of visible tasks changes and a new thumbnail or icon is updated. This list is also updated when a force fresh from getAllTaskData is requested.

Fix: 373361888
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Test: TasksRepositoryTest
Change-Id: Ifd9c54fdfcb85463c043c121fb829dec3e9faedc
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index 9c4248c..3b59864 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -40,5 +40,5 @@
      * Sets the tasks that are visible, indicating that properties relating to visuals need to be
      * populated e.g. icons/thumbnails etc.
      */
-    fun setVisibleTasks(visibleTaskIdList: List<Int>)
+    fun setVisibleTasks(visibleTaskIdList: Set<Int>)
 }
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index dc8d537..4f38ec7 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -23,147 +23,147 @@
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
 import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
-import com.android.quickstep.util.GroupTask
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChangedBy
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
-@OptIn(ExperimentalCoroutinesApi::class)
 class TasksRepository(
     private val recentsModel: RecentTasksDataSource,
     private val taskThumbnailDataSource: TaskThumbnailDataSource,
     private val taskIconDataSource: TaskIconDataSource,
     private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate,
-    recentsCoroutineScope: CoroutineScope,
+    private val recentsCoroutineScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
 ) : RecentTasksRepository {
-    private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
-    private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
-
-    private val taskData =
-        groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
-    private val visibleTasks =
-        combine(taskData, visibleTaskIds) { tasks, visibleIds ->
-            tasks.filter { it.key.id in visibleIds }
-        }
-
-    private val iconQueryResults: Flow<Map<Int, TaskIconQueryResponse?>> =
-        visibleTasks
-            .map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
-            .flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
-                if (iconRequestFlows.isEmpty()) {
-                    flowOf(emptyMap())
-                } else {
-                    combine(iconRequestFlows) { it.toMap() }
-                }
-            }
-            .distinctUntilChanged()
-
-    private val thumbnailQueryResults: Flow<Map<Int, ThumbnailData?>> =
-        visibleTasks
-            .map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
-            .flatMapLatest { thumbnailRequestFlows: List<ThumbnailDataRequest> ->
-                if (thumbnailRequestFlows.isEmpty()) {
-                    flowOf(emptyMap())
-                } else {
-                    combine(thumbnailRequestFlows) { it.toMap() }
-                }
-            }
-            .distinctUntilChanged()
-
-    private val augmentedTaskData: Flow<List<Task>> =
-        combine(taskData, thumbnailQueryResults, iconQueryResults) {
-                tasks,
-                thumbnailQueryResults,
-                iconQueryResults ->
-                tasks.onEach { task ->
-                    // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible)
-                    task.thumbnail = thumbnailQueryResults[task.key.id]
-
-                    // TODO(b/352331675) don't load icons for DesktopTaskView
-                    // Add retrieved icons + remove unnecessary icons
-                    val iconQueryResult = iconQueryResults[task.key.id]
-                    task.icon = iconQueryResult?.icon
-                    task.titleDescription = iconQueryResult?.contentDescription
-                    task.title = iconQueryResult?.title
-                }
-            }
-            .flowOn(dispatcherProvider.io)
-            .shareIn(recentsCoroutineScope, SharingStarted.WhileSubscribed(5000), replay = 1)
+    private val tasks = MutableStateFlow(MapForStateFlow<Int, Task>(emptyMap()))
+    private val taskRequests = HashMap<Int, Pair<Task.TaskKey, Job>>()
 
     override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
         if (forceRefresh) {
-            recentsModel.getTasks { groupedTaskData.value = it }
+            recentsModel.getTasks { result ->
+                tasks.value =
+                    MapForStateFlow(
+                        result
+                            .flatMap { groupTask -> groupTask.tasks }
+                            .associateBy { it.key.id }
+                            .also {
+                                // Clean tasks that are not in the latest group tasks list.
+                                val tasksNoLongerVisible = it.keys.subtract(tasks.value.keys)
+                                removeTasks(tasksNoLongerVisible)
+                            }
+                    )
+            }
         }
-        return augmentedTaskData
+        return tasks.map { it.values.toList() }
     }
 
-    override fun getTaskDataById(taskId: Int): Flow<Task?> =
-        augmentedTaskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+    override fun getTaskDataById(taskId: Int) = tasks.map { it[taskId] }
 
-    override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
+    override fun getThumbnailById(taskId: Int) =
         getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
 
-    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+    override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
         Log.d(TAG, "setVisibleTasks: $visibleTaskIdList")
-        this.visibleTaskIds.value = visibleTaskIdList.toSet()
+
+        // Remove tasks are no longer visible
+        val tasksNoLongerVisible = taskRequests.keys.subtract(visibleTaskIdList)
+        removeTasks(tasksNoLongerVisible)
+        // Add new tasks to be requested
+        visibleTaskIdList.subtract(taskRequests.keys).forEach { taskId -> requestTaskData(taskId) }
     }
 
-    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
-    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow {
-        trySend(task.key.id to task.thumbnail)
-        trySend(task.key.id to getThumbnailFromDataSource(task))
+    private fun requestTaskData(taskId: Int) {
+        Log.i(TAG, "requestTaskData: $taskId")
+        val task = tasks.value[taskId] ?: return
+        taskRequests[taskId] =
+            Pair(
+                task.key,
+                recentsCoroutineScope.launch {
+                    fetchIcon(task)
+                    fetchThumbnail(task)
+                },
+            )
+    }
 
-        val callback =
+    private fun removeTasks(tasksToRemove: Set<Int>) {
+        if (tasksToRemove.isEmpty()) return
+
+        tasksToRemove.forEach { taskId ->
+            Log.i(TAG, "removeTask: $taskId")
+            val request = taskRequests.remove(taskId) ?: return
+            val (taskKey, job) = request
+            job.cancel()
+
+            // un-registering callbacks
+            taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(taskKey)
+            taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(taskKey)
+
+            // Clearing Task to reduce memory footprint
+            tasks.value[taskId]?.apply {
+                thumbnail = null
+                icon = null
+                title = null
+                titleDescription = null
+            }
+        }
+        tasks.update { oldValue -> MapForStateFlow(oldValue) }
+    }
+
+    private suspend fun fetchIcon(task: Task) {
+        updateIcon(task.key.id, getIconFromDataSource(task)) // Fetch icon from cache
+        taskVisualsChangedDelegate.registerTaskIconChangedCallback(
+            task.key,
+            object : TaskIconChangedCallback {
+                override fun onTaskIconChanged() {
+                    recentsCoroutineScope.launch {
+                        updateIcon(task.key.id, getIconFromDataSource(task))
+                    }
+                }
+            },
+        )
+    }
+
+    private suspend fun fetchThumbnail(task: Task) {
+        updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
+        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(
+            task.key,
             object : TaskThumbnailChangedCallback {
                 override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) {
-                    trySend(task.key.id to thumbnailData)
+                    updateThumbnail(task.key.id, thumbnailData)
                 }
 
                 override fun onHighResLoadingStateChanged() {
-                    launch { trySend(task.key.id to getThumbnailFromDataSource(task)) }
+                    recentsCoroutineScope.launch {
+                        updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
+                    }
                 }
-            }
-        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback)
-        awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) }
+            },
+        )
     }
 
-    /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */
-    private fun getIconDataRequest(task: Task): IconDataRequest =
-        callbackFlow {
-                trySend(task.key.id to task.getTaskIconQueryResponse())
-                trySend(task.key.id to getIconFromDataSource(task))
+    private fun updateIcon(taskId: Int, iconData: IconData) {
+        val task = tasks.value[taskId] ?: return
+        task.icon = iconData.icon
+        task.titleDescription = iconData.contentDescription
+        task.title = iconData.title
+        tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
+    }
 
-                val callback =
-                    object : TaskIconChangedCallback {
-                        override fun onTaskIconChanged() {
-                            launch { trySend(task.key.id to getIconFromDataSource(task)) }
-                        }
-                    }
-                taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback)
-                awaitClose {
-                    taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key)
-                }
-            }
-            .distinctUntilChanged()
+    private fun updateThumbnail(taskId: Int, thumbnail: ThumbnailData?) {
+        val task = tasks.value[taskId] ?: return
+        task.thumbnail = thumbnail
+        tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
+    }
 
     private suspend fun getThumbnailFromDataSource(task: Task) =
         withContext(dispatcherProvider.main) {
@@ -184,11 +184,7 @@
                         ->
                         icon.constantState?.let {
                             continuation.resume(
-                                TaskIconQueryResponse(
-                                    it.newDrawable().mutate(),
-                                    contentDescription,
-                                    title,
-                                )
+                                IconData(it.newDrawable().mutate(), contentDescription, title)
                             )
                         }
                     }
@@ -199,22 +195,16 @@
     companion object {
         private const val TAG = "TasksRepository"
     }
+
+    /** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */
+    private data class MapForStateFlow<K, T>(
+        private val backingMap: Map<K, T>,
+        private val updated: Long = System.nanoTime(),
+    ) : Map<K, T> by backingMap
+
+    private data class IconData(
+        val icon: Drawable,
+        val contentDescription: String,
+        val title: String,
+    )
 }
-
-data class TaskIconQueryResponse(
-    val icon: Drawable,
-    val contentDescription: String,
-    val title: String,
-)
-
-private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
-    val iconVal = icon ?: return null
-    val titleDescriptionVal = titleDescription ?: return null
-    val titleVal = title ?: return null
-
-    return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal)
-}
-
-private typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
-
-private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 5cf6823..c511005 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -31,7 +31,7 @@
     }
 
     fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
-        recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
+        recentsTasksRepository.setVisibleTasks(visibleTaskIdList.toSet())
     }
 
     fun updateScale(scale: Float) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index 7a17872..d6688d6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.recents.data
 
+import android.graphics.drawable.Drawable
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import kotlinx.coroutines.flow.Flow
@@ -25,9 +26,9 @@
 
 class FakeTasksRepository : RecentTasksRepository {
     private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
-    private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
+    private var taskIconDataMap: Map<Int, FakeIconData> = emptyMap()
     private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
-    private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
+    private var visibleTasks: MutableStateFlow<Set<Int>> = MutableStateFlow(emptySet())
 
     override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks
 
@@ -48,16 +49,16 @@
     override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
         getTaskDataById(taskId).map { it?.thumbnail }
 
-    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+    override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
         visibleTasks.value = visibleTaskIdList
         tasks.value =
             tasks.value.map {
                 it.apply {
                     thumbnail = thumbnailDataMap[it.key.id]
-                    taskIconDataMap[it.key.id].let { taskIconData ->
-                        icon = taskIconData?.icon
-                        titleDescription = taskIconData?.contentDescription
-                        title = taskIconData?.title
+                    taskIconDataMap[it.key.id].let { data ->
+                        title = data?.title
+                        titleDescription = data?.titleDescription
+                        icon = data?.icon
                     }
                 }
             }
@@ -71,7 +72,14 @@
         this.thumbnailDataMap = thumbnailDataMap
     }
 
-    fun seedIconData(iconDataMap: Map<Int, TaskIconQueryResponse>) {
-        this.taskIconDataMap = iconDataMap
+    fun seedIconData(id: Int, title: String, contentDescription: String, icon: Drawable) {
+        val iconData = FakeIconData(icon, contentDescription, title)
+        this.taskIconDataMap = mapOf(id to iconData)
     }
+
+    private data class FakeIconData(
+        val icon: Drawable,
+        val titleDescription: String,
+        val title: String,
+    )
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index d55f2e3..357df6e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -74,7 +74,6 @@
     fun getAllTaskDataReturnsFlattenedListOfTasks() =
         testScope.runTest {
             recentsModel.seedTasks(defaultTaskList)
-
             assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks)
         }
 
@@ -95,7 +94,7 @@
             val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             assertThat(systemUnderTest.getTaskDataById(1).first()!!.thumbnail!!.thumbnail)
                 .isEqualTo(bitmap1)
@@ -109,7 +108,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             systemUnderTest
                 .getTaskDataById(1)
@@ -128,14 +127,14 @@
             val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
                 .isEqualTo(bitmap2)
 
             // Prevent new loading of Bitmaps
             taskThumbnailDataSource.shouldLoadSynchronously = false
-            systemUnderTest.setVisibleTasks(listOf(2, 3))
+            systemUnderTest.setVisibleTasks(setOf(2, 3))
 
             assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
                 .isEqualTo(bitmap2)
@@ -147,7 +146,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             systemUnderTest
                 .getTaskDataById(2)
@@ -156,7 +155,7 @@
 
             // Prevent new loading of Drawables
             taskThumbnailDataSource.shouldLoadSynchronously = false
-            systemUnderTest.setVisibleTasks(listOf(2, 3))
+            systemUnderTest.setVisibleTasks(setOf(2, 3))
 
             systemUnderTest
                 .getTaskDataById(2)
@@ -171,7 +170,7 @@
             val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             val task2 = systemUnderTest.getTaskDataById(2).first()!!
             assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
@@ -180,7 +179,7 @@
             // Prevent new loading of Bitmaps
             taskThumbnailDataSource.shouldLoadSynchronously = false
             taskIconDataSource.shouldLoadSynchronously = false
-            systemUnderTest.setVisibleTasks(listOf(0, 1))
+            systemUnderTest.setVisibleTasks(setOf(0, 1))
 
             val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
             assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
@@ -199,7 +198,7 @@
 
             // Setup TasksRepository
             systemUnderTest.getAllTaskData(forceRefresh = true)
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             // Assert there is no bitmap in first emission
             assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
@@ -217,8 +216,7 @@
         testScope.runTest {
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1))
+            systemUnderTest.setVisibleTasks(setOf(1))
 
             val expectedThumbnailData = createThumbnailData()
             val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
@@ -230,7 +228,7 @@
             }
             taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData)
 
-            assertThat(task1ThumbnailValues[1]!!.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.first()!!.thumbnail).isEqualTo(expectedPreviousBitmap)
             assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData)
         }
 
@@ -240,7 +238,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1))
+            systemUnderTest.setVisibleTasks(setOf(1))
 
             val expectedBitmap = mock<Bitmap>()
             val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
@@ -250,10 +248,11 @@
             testScope.backgroundScope.launch {
                 taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
             }
+
             taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
             taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
 
-            assertThat(task1ThumbnailValues[1]).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.first()).isEqualTo(expectedPreviousBitmap)
             assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
         }
 
@@ -263,7 +262,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1))
+            systemUnderTest.setVisibleTasks(setOf(1))
 
             val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable()
             val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1]
@@ -276,10 +275,34 @@
             taskIconDataSource.taskIdToDrawable[1] = expectedIcon
             taskVisualsChangedDelegate.onTaskIconChanged(1)
 
-            assertThat(task1IconValues[1]).isEqualTo(expectedPreviousIcon)
+            assertThat(task1IconValues.first()).isEqualTo(expectedPreviousIcon)
             assertThat(task1IconValues.last()).isEqualTo(expectedIcon)
         }
 
+    @Test
+    fun setVisibleTasks_multipleTimesWithDifferentTasks_reusesThumbnailRequests() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+            taskThumbnailDataSource.shouldLoadSynchronously = false
+
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+            val task1IconValues = mutableListOf<Drawable?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.icon }.toList(task1IconValues)
+            }
+
+            systemUnderTest.setVisibleTasks(setOf(1))
+            val task1UpdatingTaskOld = taskThumbnailDataSource.taskIdToUpdatingTask[1]
+            println(task1UpdatingTaskOld)
+
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+            val task1UpdatingTaskNew = taskThumbnailDataSource.taskIdToUpdatingTask[1]
+            println(task1UpdatingTaskNew)
+
+            assertThat(task1UpdatingTaskNew).isEqualTo(task1UpdatingTaskOld)
+        }
+
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000))
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
index 02f1d11..bd7d970 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -66,7 +66,7 @@
             deviceProfileRepository,
             rotationStateRepository,
             tasksRepository,
-            previewPositionHelper
+            previewPositionHelper,
         )
 
     @Test
@@ -80,7 +80,7 @@
     @Test
     fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
         tasksRepository.seedTasks(listOf(task))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
 
         assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
             .isInstanceOf(MissingThumbnail::class.java)
@@ -90,7 +90,7 @@
     fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
         tasksRepository.seedTasks(listOf(task))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
 
         val isLargeScreen = true
         deviceProfileRepository.setRecentsDeviceProfile(
@@ -119,7 +119,7 @@
                 CANVAS_HEIGHT,
                 isLargeScreen,
                 activityRotation,
-                isRtl
+                isRtl,
             )
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 12a94cf..73aa460 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -71,7 +71,7 @@
     fun taskVisible_returnsThumbnail() {
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TaskOverlayViewModelTest.TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TaskOverlayViewModelTest.TASK_ID))
 
         assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
index ba4e206..92f2efd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
@@ -71,7 +71,7 @@
                         whenever(width).thenReturn(THUMBNAIL_WIDTH)
                         whenever(height).thenReturn(THUMBNAIL_HEIGHT)
                     },
-                appearance = APPEARANCE_LIGHT_THEME
+                appearance = APPEARANCE_LIGHT_THEME,
             )
 
         val secondTask =
@@ -85,14 +85,14 @@
                         whenever(width).thenReturn(THUMBNAIL_WIDTH)
                         whenever(height).thenReturn(THUMBNAIL_HEIGHT)
                     },
-                appearance = APPEARANCE_DARK_THEME
+                appearance = APPEARANCE_DARK_THEME,
             )
 
         tasksRepository.seedTasks(listOf(firstTask, secondTask))
         tasksRepository.seedThumbnailData(
             mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
         )
-        tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(FIRST_TASK_ID, SECOND_TASK_ID))
     }
 
     companion object {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
index 829987c..a32e07d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
@@ -97,7 +97,7 @@
         )
         // setVisibleTasks forces FakeTasksRepository to update the flows returned by
         // getThumbnailById
-        tasksRepository.setVisibleTasks(listOf(1, 2))
+        tasksRepository.setVisibleTasks(setOf(1, 2))
 
         // Then wait for thumbnailData should complete, and the previous getThumbnailById flow
         // should return updated values
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
index a584d71..0767fb9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
@@ -25,7 +25,6 @@
 import android.view.Surface.ROTATION_90
 import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
 import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.data.TaskIconQueryResponse
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.systemui.shared.recents.model.Task
@@ -49,7 +48,7 @@
             taskContainerData,
             taskThumbnailViewData,
             recentTasksRepository,
-            recentsRotationStateRepository
+            recentsRotationStateRepository,
         )
 
     @Test
@@ -117,16 +116,16 @@
 
     private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
         recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
-        val expectedIconData = createIconData("Task $taskId")
-        recentTasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+        val expectedIconData = mock<Drawable>()
+        recentTasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
         recentTasksRepository.seedTasks(tasks)
-        recentTasksRepository.setVisibleTasks(listOf(taskId))
+        recentTasksRepository.setVisibleTasks(setOf(taskId))
     }
 
     private fun createThumbnailData(
         rotation: Int = Surface.ROTATION_0,
         width: Int = THUMBNAIL_WIDTH,
-        height: Int = THUMBNAIL_HEIGHT
+        height: Int = THUMBNAIL_HEIGHT,
     ): ThumbnailData {
         val bitmap = mock<Bitmap>()
         whenever(bitmap.width).thenReturn(width)
@@ -135,8 +134,6 @@
         return ThumbnailData(thumbnail = bitmap, rotation = rotation)
     }
 
-    private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
-
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
             colorBackground = Color.argb(taskId, taskId, taskId, taskId)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
index c88a3fc..c541d3d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -25,7 +25,6 @@
 import android.view.Surface
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.data.TaskIconQueryResponse
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -81,7 +80,7 @@
     fun bindRunningTask_thenStateIs_LiveTile() = runTest {
         val taskId = 1
         tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
+        tasksRepository.setVisibleTasks(setOf(taskId))
         recentsViewData.runningTaskIds.value = setOf(taskId)
         systemUnderTest.bind(taskId)
 
@@ -93,10 +92,10 @@
         val taskId = 1
         val expectedThumbnailData = createThumbnailData()
         tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
-        val expectedIconData = createIconData("Task 1")
-        tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+        val expectedIconData = mock<Drawable>()
+        tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
         tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
+        tasksRepository.setVisibleTasks(setOf(taskId))
         recentsViewData.runningTaskIds.value = setOf(taskId)
         recentsViewData.runningTaskShowScreenshot.value = true
         systemUnderTest.bind(taskId)
@@ -109,7 +108,7 @@
                         bitmap = expectedThumbnailData.thumbnail!!,
                         thumbnailRotation = Surface.ROTATION_0,
                     ),
-                    expectedIconData.icon,
+                    expectedIconData,
                 )
             )
     }
@@ -151,7 +150,7 @@
             val runningTaskId = 1
             val stoppedTaskId = 2
             tasksRepository.seedTasks(tasks)
-            tasksRepository.setVisibleTasks(listOf(runningTaskId, stoppedTaskId))
+            tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
             recentsViewData.runningTaskIds.value = setOf(runningTaskId)
             systemUnderTest.bind(runningTaskId)
             assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
@@ -165,7 +164,7 @@
     fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
         val stoppedTaskId = 2
         tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(stoppedTaskId))
+        tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
 
         systemUnderTest.bind(stoppedTaskId)
         assertThat(systemUnderTest.uiState.first())
@@ -178,7 +177,7 @@
         tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
         tasks[taskId].isLocked = true
         tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
+        tasksRepository.setVisibleTasks(setOf(taskId))
 
         systemUnderTest.bind(taskId)
         assertThat(systemUnderTest.uiState.first())
@@ -190,10 +189,10 @@
         val taskId = 2
         val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
         tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
-        val expectedIconData = createIconData("Task 2")
-        tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+        val expectedIconData = mock<Drawable>()
+        tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
         tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
+        tasksRepository.setVisibleTasks(setOf(taskId))
 
         systemUnderTest.bind(taskId)
         assertThat(systemUnderTest.uiState.first())
@@ -204,7 +203,7 @@
                         bitmap = expectedThumbnailData.thumbnail!!,
                         thumbnailRotation = Surface.ROTATION_270,
                     ),
-                    expectedIconData.icon,
+                    expectedIconData,
                 )
             )
     }
@@ -214,14 +213,14 @@
         val taskId = 2
         val expectedThumbnailData = createThumbnailData()
         tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
-        val expectedIconData = createIconData("Task 2")
-        tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+        val expectedIconData = mock<Drawable>()
+        tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
         tasksRepository.seedTasks(tasks)
 
         systemUnderTest.bind(taskId)
         assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
 
-        tasksRepository.setVisibleTasks(listOf(taskId))
+        tasksRepository.setVisibleTasks(setOf(taskId))
         assertThat(systemUnderTest.uiState.first())
             .isEqualTo(
                 SnapshotSplash(
@@ -230,7 +229,7 @@
                         bitmap = expectedThumbnailData.thumbnail!!,
                         thumbnailRotation = Surface.ROTATION_0,
                     ),
-                    expectedIconData.icon,
+                    expectedIconData,
                 )
             )
     }
@@ -295,8 +294,6 @@
         return ThumbnailData(thumbnail = bitmap, rotation = rotation)
     }
 
-    private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
-
     companion object {
         const val THUMBNAIL_WIDTH = 100
         const val THUMBNAIL_HEIGHT = 200
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index d0887df..2e91f5c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -89,12 +89,7 @@
         task.isLocked = false
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = false,
-                    thumbnail = null,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
     }
 
     @Test
@@ -103,17 +98,12 @@
         recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
         thumbnailData.isRealSnapshot = true
         task.isLocked = false
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = true,
-                    thumbnail = thumbnailData.thumbnail,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test
@@ -122,17 +112,12 @@
         recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
         thumbnailData.isRealSnapshot = true
         task.isLocked = true
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = false,
-                    thumbnail = thumbnailData.thumbnail,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test
@@ -141,17 +126,12 @@
         recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
         thumbnailData.isRealSnapshot = false
         task.isLocked = false
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = false,
-                    thumbnail = thumbnailData.thumbnail,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test