Make TasksRepository listen to changes in task visuals and update

To do this TaskVisualsChangedDelegate was added which multiplexes two listeners that are closely related.

Thumbnail overriding capabilities were also removed.

Fix: 342560598
Test: TaskVisualsChangedDelegateTest, TasksRepositoryTest
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Change-Id: If065e4179cd1f15fe2cdf9b6bae51afcb57abcc6
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index db03dac..a01ceb2 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.recents.data.RecentTasksDataSource;
+import com.android.quickstep.recents.data.TaskVisualsChangeNotifier;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TaskVisualsChangeListener;
@@ -65,7 +66,8 @@
  */
 @TargetApi(Build.VERSION_CODES.O)
 public class RecentsModel implements RecentTasksDataSource, IconChangeListener,
-        TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable {
+        TaskStackChangeListener, TaskVisualsChangeListener, TaskVisualsChangeNotifier,
+        SafeCloseable {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
@@ -287,6 +289,7 @@
     /**
      * Adds a listener for visuals changes
      */
+    @Override
     public void addThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.add(listener);
     }
@@ -294,6 +297,7 @@
     /**
      * Removes a previously added listener
      */
+    @Override
     public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.remove(listener);
     }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 3c6c3e4..580dcc2 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.recents.data.HighResLoadingStateNotifier;
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
 import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
 import com.android.quickstep.util.TaskKeyCache;
@@ -48,7 +49,7 @@
     private final boolean mEnableTaskSnapshotPreloading;
     private final Context mContext;
 
-    public static class HighResLoadingState {
+    public static class HighResLoadingState implements HighResLoadingStateNotifier {
         private boolean mForceHighResThumbnails;
         private boolean mVisible;
         private boolean mFlingingFast;
@@ -65,11 +66,13 @@
             mForceHighResThumbnails = !supportsLowResThumbnails();
         }
 
-        public void addCallback(HighResLoadingStateChangedCallback callback) {
+        @Override
+        public void addCallback(@NonNull HighResLoadingStateChangedCallback callback) {
             mCallbacks.add(callback);
         }
 
-        public void removeCallback(HighResLoadingStateChangedCallback callback) {
+        @Override
+        public void removeCallback(@NonNull HighResLoadingStateChangedCallback callback) {
             mCallbacks.remove(callback);
         }
 
diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
new file mode 100644
index 0000000..df546ca
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+
+/** Notifies added callbacks that high res state has changed */
+interface HighResLoadingStateNotifier {
+    /** Adds a callback for high res loading state */
+    fun addCallback(callback: HighResLoadingStateChangedCallback)
+
+    /** Removes a callback for high res loading state */
+    fun removeCallback(callback: HighResLoadingStateChangedCallback)
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index 4f7a541..9c4248c 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -41,10 +41,4 @@
      * populated e.g. icons/thumbnails etc.
      */
     fun setVisibleTasks(visibleTaskIdList: List<Int>)
-
-    /**
-     * Override [ThumbnailData] with a map of taskId to [ThumbnailData]. The override only applies
-     * if the tasks are already visible, and will be invalidated when tasks become invisible.
-     */
-    fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>)
 }
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt
new file mode 100644
index 0000000..6e7789d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.TaskVisualsChangeListener
+
+/** Notifies added listeners that task visuals have changed */
+interface TaskVisualsChangeNotifier {
+    /** Adds a listener for visuals changes */
+    fun addThumbnailChangeListener(listener: TaskVisualsChangeListener)
+
+    /** Removes a listener for visuals changes */
+    fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener)
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
new file mode 100644
index 0000000..a141e89
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.os.UserHandle
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
+import com.android.quickstep.util.TaskVisualsChangeListener
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+
+/** Delegates the checking of task visuals (thumbnails, high res changes, icons) */
+interface TaskVisualsChangedDelegate :
+    TaskVisualsChangeListener, HighResLoadingStateChangedCallback {
+    /** Registers a callback for visuals relating to icons */
+    fun registerTaskIconChangedCallback(
+        taskKey: Task.TaskKey,
+        taskIconChangedCallback: TaskIconChangedCallback
+    )
+
+    /** Unregisters a callback for visuals relating to icons */
+    fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey)
+
+    /** Registers a callback for visuals relating to thumbnails */
+    fun registerTaskThumbnailChangedCallback(
+        taskKey: Task.TaskKey,
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback
+    )
+
+    /** Unregisters a callback for visuals relating to thumbnails */
+    fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey)
+
+    /** A callback for task icon changes */
+    interface TaskIconChangedCallback {
+        /** Informs the listener that the task icon has changed */
+        fun onTaskIconChanged()
+    }
+
+    /** A callback for task thumbnail changes */
+    interface TaskThumbnailChangedCallback {
+        /** Informs the listener that the task thumbnail data has changed to [thumbnailData] */
+        fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?)
+
+        /** Informs the listener that the default resolution for loading thumbnails has changed */
+        fun onHighResLoadingStateChanged()
+    }
+}
+
+class TaskVisualsChangedDelegateImpl(
+    private val taskVisualsChangeNotifier: TaskVisualsChangeNotifier,
+    private val highResLoadingStateNotifier: HighResLoadingStateNotifier,
+) : TaskVisualsChangedDelegate {
+    private val taskIconChangedCallbacks =
+        mutableMapOf<Int, Pair<Task.TaskKey, TaskIconChangedCallback>>()
+    private val taskThumbnailChangedCallbacks =
+        mutableMapOf<Int, Pair<Task.TaskKey, TaskThumbnailChangedCallback>>()
+    private var isListening = false
+
+    @Synchronized
+    private fun onCallbackRegistered() {
+        if (isListening) return
+
+        taskVisualsChangeNotifier.addThumbnailChangeListener(this)
+        highResLoadingStateNotifier.addCallback(this)
+        isListening = true
+    }
+
+    @Synchronized
+    private fun onCallbackUnregistered() {
+        if (!isListening) return
+
+        if (taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size == 0) {
+            taskVisualsChangeNotifier.removeThumbnailChangeListener(this)
+            highResLoadingStateNotifier.removeCallback(this)
+        }
+
+        isListening = false
+    }
+
+    override fun onTaskIconChanged(taskId: Int) {
+        taskIconChangedCallbacks[taskId]?.let { (_, callback) -> callback.onTaskIconChanged() }
+    }
+
+    override fun onTaskIconChanged(pkg: String, user: UserHandle) {
+        taskIconChangedCallbacks.values
+            .filter { (taskKey, _) ->
+                pkg == taskKey.packageName && user.identifier == taskKey.userId
+            }
+            .forEach { (_, callback) -> callback.onTaskIconChanged() }
+    }
+
+    override fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData?): Task? {
+        taskThumbnailChangedCallbacks[taskId]?.let { (_, callback) ->
+            callback.onTaskThumbnailChanged(thumbnailData)
+        }
+        return null
+    }
+
+    override fun onHighResLoadingStateChanged(enabled: Boolean) {
+        taskThumbnailChangedCallbacks.values.forEach { (_, callback) ->
+            callback.onHighResLoadingStateChanged()
+        }
+    }
+
+    override fun registerTaskIconChangedCallback(
+        taskKey: Task.TaskKey,
+        taskIconChangedCallback: TaskIconChangedCallback
+    ) {
+        taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback
+        onCallbackRegistered()
+    }
+
+    override fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) {
+        taskIconChangedCallbacks.remove(taskKey.id)
+        onCallbackUnregistered()
+    }
+
+    override fun registerTaskThumbnailChangedCallback(
+        taskKey: Task.TaskKey,
+        taskThumbnailChangedCallback: TaskThumbnailChangedCallback
+    ) {
+        taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback
+        onCallbackRegistered()
+    }
+
+    override fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) {
+        taskThumbnailChangedCallbacks.remove(taskKey.id)
+        onCallbackUnregistered()
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 6f9d157..eb3c2d1 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -18,6 +18,8 @@
 
 import android.graphics.drawable.Drawable
 import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
 import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
 import com.android.quickstep.util.GroupTask
@@ -26,18 +28,20 @@
 import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
@@ -46,12 +50,12 @@
     private val recentsModel: RecentTasksDataSource,
     private val taskThumbnailDataSource: TaskThumbnailDataSource,
     private val taskIconDataSource: TaskIconDataSource,
+    private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate,
     recentsCoroutineScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
 ) : RecentTasksRepository {
     private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
     private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
-    private val thumbnailOverride = MutableStateFlow(mapOf<Int, ThumbnailData>())
 
     private val taskData =
         groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
@@ -85,15 +89,13 @@
             .distinctUntilChanged()
 
     private val augmentedTaskData: Flow<List<Task>> =
-        combine(taskData, thumbnailQueryResults, iconQueryResults, thumbnailOverride) {
+        combine(taskData, thumbnailQueryResults, iconQueryResults) {
                 tasks,
                 thumbnailQueryResults,
-                iconQueryResults,
-                thumbnailOverride ->
+                iconQueryResults ->
                 tasks.onEach { task ->
                     // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible)
-                    task.thumbnail =
-                        thumbnailOverride[task.key.id] ?: thumbnailQueryResults[task.key.id]
+                    task.thumbnail = thumbnailQueryResults[task.key.id]
 
                     // TODO(b/352331675) don't load icons for DesktopTaskView
                     // Add retrieved icons + remove unnecessary icons
@@ -121,61 +123,76 @@
 
     override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         this.visibleTaskIds.value = visibleTaskIdList.toSet()
-        addOrUpdateThumbnailOverride(emptyMap())
-    }
-
-    override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
-        this.thumbnailOverride.value =
-            this.thumbnailOverride.value
-                .toMutableMap()
-                .apply { putAll(thumbnailOverride) }
-                .filterKeys(this.visibleTaskIds.value::contains)
     }
 
     /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
-    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = flow {
-        emit(task.key.id to task.thumbnail)
-        val thumbnailDataResult: ThumbnailData? =
-            withContext(dispatcherProvider.main) {
-                suspendCancellableCoroutine { continuation ->
-                    val cancellableTask =
-                        taskThumbnailDataSource.getThumbnailInBackground(task) {
-                            continuation.resume(it)
-                        }
-                    continuation.invokeOnCancellation { cancellableTask?.cancel() }
+    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow {
+        trySend(task.key.id to task.thumbnail)
+        trySend(task.key.id to getThumbnailFromDataSource(task))
+
+        val callback =
+            object : TaskThumbnailChangedCallback {
+                override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) {
+                    trySend(task.key.id to thumbnailData)
+                }
+
+                override fun onHighResLoadingStateChanged() {
+                    launch { trySend(task.key.id to getThumbnailFromDataSource(task)) }
                 }
             }
-        emit(task.key.id to thumbnailDataResult)
+        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback)
+        awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) }
     }
 
-    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
+    /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */
     private fun getIconDataRequest(task: Task): IconDataRequest =
-        flow {
-                emit(task.key.id to task.getTaskIconQueryResponse())
-                val iconDataResponse: TaskIconQueryResponse? =
-                    withContext(dispatcherProvider.main) {
-                        suspendCancellableCoroutine { continuation ->
-                            val cancellableTask =
-                                taskIconDataSource.getIconInBackground(task) {
-                                    icon,
-                                    contentDescription,
-                                    title ->
-                                    icon.constantState?.let {
-                                        continuation.resume(
-                                            TaskIconQueryResponse(
-                                                it.newDrawable().mutate(),
-                                                contentDescription,
-                                                title
-                                            )
-                                        )
-                                    }
-                                }
-                            continuation.invokeOnCancellation { cancellableTask?.cancel() }
+        callbackFlow {
+                trySend(task.key.id to task.getTaskIconQueryResponse())
+                trySend(task.key.id to getIconFromDataSource(task))
+
+                val callback =
+                    object : TaskIconChangedCallback {
+                        override fun onTaskIconChanged() {
+                            launch { trySend(task.key.id to getIconFromDataSource(task)) }
                         }
                     }
-                emit(task.key.id to iconDataResponse)
+                taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback)
+                awaitClose {
+                    taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key)
+                }
             }
             .distinctUntilChanged()
+
+    private suspend fun getThumbnailFromDataSource(task: Task) =
+        withContext(dispatcherProvider.main) {
+            suspendCancellableCoroutine { continuation ->
+                val cancellableTask =
+                    taskThumbnailDataSource.getThumbnailInBackground(task) {
+                        continuation.resume(it)
+                    }
+                continuation.invokeOnCancellation { cancellableTask?.cancel() }
+            }
+        }
+
+    private suspend fun getIconFromDataSource(task: Task) =
+        withContext(dispatcherProvider.main) {
+            suspendCancellableCoroutine { continuation ->
+                val cancellableTask =
+                    taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title
+                        ->
+                        icon.constantState?.let {
+                            continuation.resume(
+                                TaskIconQueryResponse(
+                                    it.newDrawable().mutate(),
+                                    contentDescription,
+                                    title
+                                )
+                            )
+                        }
+                    }
+                continuation.invokeOnCancellation { cancellableTask?.cancel() }
+            }
+        }
 }
 
 data class TaskIconQueryResponse(
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 0b672d1..0a5544f 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -22,6 +22,8 @@
 import com.android.launcher3.util.coroutines.ProductionDispatchers
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl
 import com.android.quickstep.recents.data.TasksRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.GetThumbnailUseCase
@@ -60,14 +62,22 @@
             val recentsCoroutineScope =
                 CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView"))
             set(CoroutineScope::class.java.simpleName, recentsCoroutineScope)
+            val recentsModel = RecentsModel.INSTANCE.get(appContext)
+            val taskVisualsChangedDelegate =
+                TaskVisualsChangedDelegateImpl(
+                    recentsModel,
+                    recentsModel.thumbnailCache.highResLoadingState
+                )
+            set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate)
 
             // Create RecentsTaskRepository singleton
             val recentTasksRepository: RecentTasksRepository =
-                with(RecentsModel.INSTANCE.get(appContext)) {
+                with(recentsModel) {
                     TasksRepository(
                         this,
                         thumbnailCache,
                         iconCache,
+                        taskVisualsChangedDelegate,
                         recentsCoroutineScope,
                         ProductionDispatchers
                     )
@@ -155,6 +165,7 @@
                             thumbnailCache,
                             iconCache,
                             get(),
+                            get(),
                             ProductionDispatchers
                         )
                     }
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index b1f46a3..1716f2e 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -58,10 +58,6 @@
         recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
     }
 
-    fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
-        recentsTasksRepository.addOrUpdateThumbnailOverride(thumbnailOverride)
-    }
-
     suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>) {
         combine(
                 updatedThumbnails.map {
diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
index 66bff73..519ef60 100644
--- a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.util;
 
+import android.annotation.NonNull;
 import android.os.UserHandle;
 
 import com.android.systemui.shared.recents.model.Task;
@@ -36,7 +37,7 @@
     /**
      * Called when the icon for a task changes
      */
-    default void onTaskIconChanged(String pkg, UserHandle user) {}
+    default void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {}
 
     /**
      * Called when the icon for a task changes
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 69a9690..255619a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1079,7 +1079,6 @@
     @Nullable
     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
         if (enableRefactorTaskThumbnail()) {
-            mHelper.onTaskThumbnailChanged(taskId, thumbnailData);
             return null;
         }
         if (mHandleTaskStackChanges) {
@@ -1100,8 +1099,7 @@
     }
 
     @Override
-    public void onTaskIconChanged(String pkg, UserHandle user) {
-        // TODO(b/342560598): Listen in TaskRepository and reload.
+    public void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView tv = requireTaskViewAt(i);
             Task task = tv.getFirstTask();
@@ -2549,7 +2547,6 @@
         }
 
         if (enableRefactorTaskThumbnail()) {
-            // TODO(b/342560598): Listen in TaskRepository and reload.
             return;
         }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index 5b71da1..f5b2176 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -68,8 +68,4 @@
             ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
         }
     }
-
-    fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData) {
-        recentsViewModel.addOrUpdateThumbnailOverride(mapOf(taskId to thumbnailData))
-    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
new file mode 100644
index 0000000..7d09efd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+
+class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier {
+    val listeners = mutableListOf<HighResLoadingStateChangedCallback>()
+
+    override fun addCallback(callback: HighResLoadingStateChangedCallback) {
+        listeners.add(callback)
+    }
+
+    override fun removeCallback(callback: HighResLoadingStateChangedCallback) {
+        listeners.remove(callback)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
index fee4979..5de876a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -27,7 +27,8 @@
 
 class FakeTaskIconDataSource : TaskIconDataSource {
 
-    val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mockCopyableDrawable() }
+    val taskIdToDrawable: MutableMap<Int, Drawable> =
+        (0..10).associateWith { mockCopyableDrawable() }.toMutableMap()
 
     val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
     var shouldLoadSynchronously: Boolean = true
@@ -52,15 +53,17 @@
         return null
     }
 
-    private fun mockCopyableDrawable(): Drawable {
-        val mutableDrawable = mock<Drawable>()
-        val immutableDrawable =
-            mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
-        val constantState =
-            mock<Drawable.ConstantState>().apply {
-                whenever(newDrawable()).thenReturn(immutableDrawable)
-            }
-        return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
+    companion object {
+        fun mockCopyableDrawable(): Drawable {
+            val mutableDrawable = mock<Drawable>()
+            val immutableDrawable =
+                mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
+            val constantState =
+                mock<Drawable.ConstantState>().apply {
+                    whenever(newDrawable()).thenReturn(immutableDrawable)
+                }
+            return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
+        }
     }
 }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index 30fc491..d12c0b0 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -27,7 +27,8 @@
 
 class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
 
-    val taskIdToBitmap: Map<Int, Bitmap> = (0..10).associateWith { mock() }
+    val taskIdToBitmap: MutableMap<Int, Bitmap> =
+        (0..10).associateWith { mock<Bitmap>() }.toMutableMap()
     val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
     var shouldLoadSynchronously: Boolean = true
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt
new file mode 100644
index 0000000..765f0d1
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.TaskVisualsChangeListener
+
+class FakeTaskVisualsChangeNotifier : TaskVisualsChangeNotifier {
+    val listeners = mutableListOf<TaskVisualsChangeListener>()
+
+    override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) {
+        listeners.remove(listener)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index d94a351..7a17872 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -28,7 +28,6 @@
     private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
     private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
     private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
-    private var thumbnailOverrideMap: Map<Int, ThumbnailData> = emptyMap()
 
     override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks
 
@@ -39,7 +38,7 @@
             .map { taskList ->
                 val task = taskList.firstOrNull { it.key.id == taskId } ?: return@map null
                 Task(task).apply {
-                    thumbnail = thumbnailOverrideMap[taskId] ?: task.thumbnail
+                    thumbnail = task.thumbnail
                     icon = task.icon
                     titleDescription = task.titleDescription
                     title = task.title
@@ -62,16 +61,6 @@
                     }
                 }
             }
-        setThumbnailOverrideInternal(thumbnailOverrideMap)
-    }
-
-    override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
-        setThumbnailOverrideInternal(thumbnailOverride)
-    }
-
-    private fun setThumbnailOverrideInternal(thumbnailOverride: Map<Int, ThumbnailData>) {
-        thumbnailOverrideMap =
-            thumbnailOverride.filterKeys(this.visibleTasks.value::contains).toMap()
     }
 
     fun seedTasks(tasks: List<Task>) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
new file mode 100644
index 0000000..41f6bfd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.UserHandle
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
+import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+class TaskVisualsChangedDelegateTest {
+    private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
+    private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
+
+    val systemUnderTest =
+        TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)
+
+    @Test
+    fun addingFirstListener_addsListenerToNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+
+        assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest)
+        assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest)
+    }
+
+    @Test
+    fun addingAndRemovingListener_removesListenerFromNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+        systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1))
+
+        assertThat(taskVisualsChangeNotifier.listeners).isEmpty()
+        assertThat(highResLoadingStateNotifier.listeners).isEmpty()
+    }
+
+    @Test
+    fun addingTwoAndRemovingOneListener_doesNotRemoveListenerFromNotifiers() {
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock())
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 2), mock())
+        systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1))
+
+        assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest)
+        assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest)
+    }
+
+    @Test
+    fun onTaskIconChangedWithTaskId_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskIconChangedCallback>()
+        val additionalListener = mock<TaskIconChangedCallback>()
+        systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 1), expectedListener)
+        systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 2), additionalListener)
+
+        systemUnderTest.onTaskIconChanged(1)
+
+        verify(expectedListener).onTaskIconChanged()
+        verifyNoMoreInteractions(additionalListener)
+    }
+
+    @Test
+    fun onTaskIconChangedWithoutTaskId_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskIconChangedCallback>()
+        val listener = mock<TaskIconChangedCallback>()
+        // Correct match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            expectedListener
+        )
+        // 1 out of 2 match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1),
+            listener
+        )
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2),
+            listener
+        )
+        // 0 out of 2 match
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2),
+            listener
+        )
+
+        systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
+
+        verify(expectedListener).onTaskIconChanged()
+        verifyNoMoreInteractions(listener)
+    }
+
+    @Test
+    fun replacedTaskIconChangedCallbacks_notCalled() {
+        val replacedListener = mock<TaskIconChangedCallback>()
+        val newListener = mock<TaskIconChangedCallback>()
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            replacedListener
+        )
+        systemUnderTest.registerTaskIconChangedCallback(
+            createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
+            newListener
+        )
+
+        systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
+
+        verifyNoMoreInteractions(replacedListener)
+        verify(newListener).onTaskIconChanged()
+    }
+
+    @Test
+    fun onTaskThumbnailChanged_notifiesCorrectListenerOnly() {
+        val expectedListener = mock<TaskThumbnailChangedCallback>()
+        val additionalListener = mock<TaskThumbnailChangedCallback>()
+        val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            expectedListener
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 2),
+            additionalListener
+        )
+
+        systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+        verify(expectedListener).onTaskThumbnailChanged(expectedThumbnailData)
+        verifyNoMoreInteractions(additionalListener)
+    }
+
+    @Test
+    fun onHighResLoadingStateChanged_notifiesAllListeners() {
+        val expectedListener = mock<TaskThumbnailChangedCallback>()
+        val additionalListener = mock<TaskThumbnailChangedCallback>()
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            expectedListener
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 2),
+            additionalListener
+        )
+
+        systemUnderTest.onHighResLoadingStateChanged(true)
+
+        verify(expectedListener).onHighResLoadingStateChanged()
+        verify(additionalListener).onHighResLoadingStateChanged()
+    }
+
+    @Test
+    fun replacedTaskThumbnailChangedCallbacks_notCalled() {
+        val replacedListener1 = mock<TaskThumbnailChangedCallback>()
+        val newListener1 = mock<TaskThumbnailChangedCallback>()
+        val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            replacedListener1
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1)
+
+        systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+        verifyNoMoreInteractions(replacedListener1)
+        verify(newListener1).onTaskThumbnailChanged(expectedThumbnailData)
+    }
+
+    private fun createTaskKey(id: Int = 1, pkg: String = PACKAGE_NAME, userId: Int = 1) =
+        TaskKey(id, 0, Intent().setPackage(pkg), ComponentName("", ""), userId, 0)
+
+    private companion object {
+        const val PACKAGE_NAME = "com.test.test"
+        const val ALTERNATIVE_PACKAGE_NAME = "com.test.test2"
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index e6534eb..f31467f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -19,7 +19,7 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.graphics.Bitmap
-import android.view.Surface
+import android.graphics.drawable.Drawable
 import com.android.launcher3.util.TestDispatcherProvider
 import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest
 import com.android.quickstep.util.DesktopTask
@@ -29,6 +29,9 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
@@ -48,6 +51,10 @@
     private val recentsModel = FakeRecentTasksDataSource()
     private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
     private val taskIconDataSource = FakeTaskIconDataSource()
+    private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
+    private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
+    private val taskVisualsChangedDelegate =
+        TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)
 
     private val dispatcher = UnconfinedTestDispatcher()
     private val testScope = TestScope(dispatcher)
@@ -56,6 +63,7 @@
             recentsModel,
             taskThumbnailDataSource,
             taskIconDataSource,
+            taskVisualsChangedDelegate,
             testScope.backgroundScope,
             TestDispatcherProvider(dispatcher)
         )
@@ -203,87 +211,81 @@
         }
 
     @Test
-    fun addThumbnailOverrideOverrideThumbnails() =
+    fun onTaskThumbnailChanged_setsNewThumbnailDataOnTask() =
         testScope.runTest {
             recentsModel.seedTasks(defaultTaskList)
-            val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-            val thumbnailOverride2 = createThumbnailData()
-            systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
-
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride2.thumbnail)
-        }
-
-    @Test
-    fun addThumbnailOverrideMultipleOverrides() =
-        testScope.runTest {
-            recentsModel.seedTasks(defaultTaskList)
-            val thumbnailOverride1 = createThumbnailData()
-            val thumbnailOverride2 = createThumbnailData()
-            val thumbnailOverride3 = createThumbnailData()
-            systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride3))
-
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride1.thumbnail)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride3.thumbnail)
-        }
-
-    @Test
-    fun addThumbnailOverrideClearedWhenTaskBecomeInvisible() =
-        testScope.runTest {
-            recentsModel.seedTasks(defaultTaskList)
-            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-            val thumbnailOverride1 = createThumbnailData()
-            val thumbnailOverride2 = createThumbnailData()
-            systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2))
-            // Making task 2 invisible and visible again should clear the override
-            systemUnderTest.setVisibleTasks(listOf(1))
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
-
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail)
-                .isEqualTo(thumbnailOverride1.thumbnail)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2)
-        }
-
-    @Test
-    fun addThumbnailOverrideDoesNotOverrideInvisibleTasks() =
-        testScope.runTest {
-            recentsModel.seedTasks(defaultTaskList)
-            val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
-            val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
-            val thumbnailOverride = createThumbnailData()
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
             systemUnderTest.setVisibleTasks(listOf(1))
-            systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride))
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
 
-            assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1)
-            assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2)
+            val expectedThumbnailData = createThumbnailData()
+            val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
+            }
+            taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData)
+
+            assertThat(task1ThumbnailValues[1]!!.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData)
+        }
+
+    @Test
+    fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(listOf(1))
+
+            val expectedBitmap = mock<Bitmap>()
+            val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1ThumbnailValues = mutableListOf<Bitmap?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
+            }
+            taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+            taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
+
+            assertThat(task1ThumbnailValues[1]).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
+        }
+
+    @Test
+    fun onTaskIconChanged_setsNewIconOnTask() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(listOf(1))
+
+            val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable()
+            val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1IconValues = mutableListOf<Drawable?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.icon }.toList(task1IconValues)
+            }
+            taskIconDataSource.taskIdToDrawable[1] = expectedIcon
+            taskVisualsChangedDelegate.onTaskIconChanged(1)
+
+            assertThat(task1IconValues[1]).isEqualTo(expectedPreviousIcon)
+            assertThat(task1IconValues.last()).isEqualTo(expectedIcon)
         }
 
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000))
 
-    private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
+    private fun createThumbnailData(): ThumbnailData {
         val bitmap = mock<Bitmap>()
         whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH)
         whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT)
 
-        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+        return ThumbnailData(thumbnail = bitmap)
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
index dc16475..fe67313 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
@@ -70,44 +70,6 @@
         assertThat(thumbnailDataFlow2.first()).isNull()
     }
 
-    @Test
-    fun thumbnailOverrideWaitAndReset() = runTest {
-        val thumbnailData1 = createThumbnailData().apply { snapshotId = 1 }
-        val thumbnailData2 = createThumbnailData().apply { snapshotId = 2 }
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2))
-
-        val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1)
-        val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2)
-
-        systemUnderTest.refreshAllTaskData()
-        systemUnderTest.updateVisibleTasks(listOf(1, 2))
-
-        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
-        assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2)
-
-        systemUnderTest.setRunningTaskShowScreenshot(true)
-        val thumbnailOverride = mapOf(2 to createThumbnailData().apply { snapshotId = 3 })
-        systemUnderTest.addOrUpdateThumbnailOverride(thumbnailOverride)
-
-        systemUnderTest.waitForRunningTaskShowScreenshotToUpdate()
-        val expectedUpdate = mapOf(2 to createThumbnailData().apply { snapshotId = 3 })
-        systemUnderTest.waitForThumbnailsToUpdate(expectedUpdate)
-
-        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
-        assertThat(thumbnailDataFlow2.first()?.snapshotId).isEqualTo(3)
-
-        systemUnderTest.onReset()
-
-        assertThat(thumbnailDataFlow1.first()).isNull()
-        assertThat(thumbnailDataFlow2.first()).isNull()
-
-        systemUnderTest.updateVisibleTasks(listOf(1, 2))
-
-        assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1)
-        assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2)
-    }
-
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
             colorBackground = Color.argb(taskId, taskId, taskId, taskId)