Add TasksRepository

Bug: 334825222
Test: TasksRepositoryTest
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Change-Id: I3e08dea7b205df54f8bef456ead6466aa2ce45c6
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index b213203..358d703 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -148,7 +148,7 @@
         });
     }
 
-    private void processLoadedTasks(ArrayList<GroupTask> tasks) {
+    private void processLoadedTasks(List<GroupTask> tasks) {
         // Only store MAX_TASK tasks, from most to least recent
         Collections.reverse(tasks);
         mTasks = tasks.stream()
@@ -157,7 +157,7 @@
         mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS);
     }
 
-    private void processLoadedTasksOnDesktop(ArrayList<GroupTask> tasks) {
+    private void processLoadedTasksOnDesktop(List<GroupTask> tasks) {
         // Find the single desktop task that contains a grouping of desktop tasks
         DesktopTask desktopTask = findDesktopTask(tasks);
 
@@ -173,7 +173,7 @@
     }
 
     @Nullable
-    private DesktopTask findDesktopTask(ArrayList<GroupTask> tasks) {
+    private DesktopTask findDesktopTask(List<GroupTask> tasks) {
         return (DesktopTask) tasks.stream()
                 .filter(t -> t instanceof DesktopTask)
                 .findFirst()
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 711882c..37b4dca 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -31,6 +31,7 @@
 import android.os.RemoteException;
 import android.util.SparseBooleanArray;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.util.LooperExecutor;
@@ -44,6 +45,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -137,7 +139,7 @@
      * @return The change id of the current task list
      */
     public synchronized int getTasks(boolean loadKeysOnly,
-            Consumer<ArrayList<GroupTask>> callback, Predicate<GroupTask> filter) {
+            @Nullable Consumer<List<GroupTask>> callback, Predicate<GroupTask> filter) {
         final int requestLoadId = mChangeId;
         if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) {
             // The list is up to date, send the callback on the next frame,
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 89351aa..98c1eb4 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -33,6 +33,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.icons.IconProvider;
@@ -40,6 +41,7 @@
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
+import com.android.quickstep.recents.data.RecentTasksDataSource;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -60,8 +62,8 @@
  * Singleton class to load and manage recents model.
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class RecentsModel implements IconChangeListener, TaskStackChangeListener,
-        TaskVisualsChangeListener, SafeCloseable {
+public class RecentsModel implements RecentTasksDataSource, IconChangeListener,
+        TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
@@ -141,7 +143,8 @@
      *                always called on the UI thread.
      * @return the request id associated with this call.
      */
-    public int getTasks(Consumer<ArrayList<GroupTask>> callback) {
+    @Override
+    public int getTasks(@Nullable Consumer<List<GroupTask>> callback) {
         return mTaskList.getTasks(false /* loadKeysOnly */, callback,
                 RecentsFilterState.DEFAULT_FILTER);
     }
@@ -155,7 +158,7 @@
      *                callback.
      * @return the request id associated with this call.
      */
-    public int getTasks(Consumer<ArrayList<GroupTask>> callback, Predicate<GroupTask> filter) {
+    public int getTasks(@Nullable Consumer<List<GroupTask>> callback, Predicate<GroupTask> filter) {
         return mTaskList.getTasks(false /* loadKeysOnly */, callback, filter);
     }
 
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 7ebb767..38e927f 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -21,11 +21,13 @@
 import android.content.Context;
 import android.content.res.Resources;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
 import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
 import com.android.quickstep.util.TaskKeyCache;
 import com.android.quickstep.util.TaskKeyLruCache;
@@ -38,7 +40,7 @@
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
-public class TaskThumbnailCache {
+public class TaskThumbnailCache implements TaskThumbnailDataSource {
 
     private final Executor mBgExecutor;
     private final TaskKeyCache<ThumbnailData> mCache;
@@ -148,8 +150,9 @@
      * @param callback The callback to receive the task after its data has been populated.
      * @return A cancelable handle to the request
      */
+    @Override
     public CancellableTask<ThumbnailData> updateThumbnailInBackground(
-            Task task, Consumer<ThumbnailData> callback) {
+            Task task, @NonNull Consumer<ThumbnailData> callback) {
         Preconditions.assertUIThread();
 
         boolean lowResolution = !mHighResLoadingState.isEnabled();
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 096ed2c..485d6c4 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -54,6 +54,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 public class FallbackRecentsView extends RecentsView<RecentsActivity, RecentsState>
         implements StateListener<RecentsState> {
@@ -179,7 +180,7 @@
     }
 
     @Override
-    protected void applyLoadPlan(ArrayList<GroupTask> taskGroups) {
+    protected void applyLoadPlan(List<GroupTask> taskGroups) {
         // When quick-switching on 3p-launcher, we add a "stub" tile corresponding to Launcher
         // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
         // track the index of the next task appropriately, as if we are switching on any other app.
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt
new file mode 100644
index 0000000..6719099
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.GroupTask
+import java.util.function.Consumer
+
+interface RecentTasksDataSource {
+    fun getTasks(callback: Consumer<List<GroupTask>>?): Int
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
new file mode 100644
index 0000000..ad8ae20
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.TaskIconCache
+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.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class TasksRepository(
+    private val recentsModel: RecentTasksDataSource,
+    private val taskThumbnailDataSource: TaskThumbnailDataSource,
+    private val taskIconCache: TaskIconCache,
+) {
+    private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
+    private val _taskData =
+        groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
+    private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
+
+    private val taskData: Flow<List<Task>> =
+        combine(_taskData, getThumbnailQueryResults()) { tasks, results ->
+            tasks.forEach { task ->
+                // Add retrieved thumbnails + remove unnecessary thumbnails
+                task.thumbnail = results[task.key.id]
+            }
+            tasks
+        }
+
+    fun getAllTaskData(forceRefresh: Boolean = false): Flow<List<Task>> {
+        if (forceRefresh) {
+            recentsModel.getTasks { groupedTaskData.value = it }
+        }
+        return taskData
+    }
+
+    fun getTaskDataById(taskId: Int): Flow<Task?> =
+        taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+
+    fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+        this.visibleTaskIds.value = visibleTaskIdList.toSet()
+    }
+
+    /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */
+    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest =
+        flow {
+                emit(task.key.id to task.thumbnail)
+                val thumbnailDataResult: ThumbnailData? =
+                    suspendCancellableCoroutine { continuation ->
+                        val cancellableTask =
+                            taskThumbnailDataSource.updateThumbnailInBackground(task) {
+                                continuation.resume(it)
+                            }
+                        continuation.invokeOnCancellation { cancellableTask?.cancel() }
+                    }
+                emit(task.key.id to thumbnailDataResult)
+            }
+            .distinctUntilChanged()
+
+    /**
+     * This is a Flow that makes a query for thumbnail data to the [taskThumbnailDataSource] for
+     * each visible task. It then collects the responses and returns them in a Map as soon as they
+     * are available.
+     */
+    private fun getThumbnailQueryResults(): Flow<Map<Int, ThumbnailData?>> {
+        val visibleTasks =
+            combine(_taskData, visibleTaskIds) { tasks, visibleIds ->
+                tasks.filter { it.key.id in visibleIds }
+            }
+        val visibleThumbnailDataRequests: Flow<List<ThumbnailDataRequest>> =
+            visibleTasks.map {
+                it.map { visibleTask ->
+                    val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail }
+                    getThumbnailDataRequest(taskCopy)
+                }
+            }
+        return visibleThumbnailDataRequests.flatMapLatest {
+            thumbnailRequestFlows: List<ThumbnailDataRequest> ->
+            if (thumbnailRequestFlows.isEmpty()) {
+                flowOf(emptyMap())
+            } else {
+                combine(thumbnailRequestFlows) { it.toMap() }
+            }
+        }
+    }
+}
+
+typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
new file mode 100644
index 0000000..55598f0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.task.thumbnail.data
+
+import com.android.launcher3.util.CancellableTask
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import java.util.function.Consumer
+
+interface TaskThumbnailDataSource {
+    fun updateThumbnailInBackground(
+        task: Task,
+        callback: Consumer<ThumbnailData>
+    ): CancellableTask<ThumbnailData>?
+}
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index 07f2d68..8d99069 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -21,7 +21,7 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 
-import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A {@link Task} container that can contain N number of tasks that are part of the desktop in
@@ -30,9 +30,9 @@
 public class DesktopTask extends GroupTask {
 
     @NonNull
-    public final ArrayList<Task> tasks;
+    public final List<Task> tasks;
 
-    public DesktopTask(@NonNull ArrayList<Task> tasks) {
+    public DesktopTask(@NonNull List<Task> tasks) {
         super(tasks.get(0), null, null, TaskView.Type.DESKTOP);
         this.tasks = tasks;
     }
@@ -53,6 +53,12 @@
     }
 
     @Override
+    @NonNull
+    public List<Task> getTasks() {
+        return tasks;
+    }
+
+    @Override
     public DesktopTask copy() {
         return new DesktopTask(tasks);
     }
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index 7dd6afc..945ffe3 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -23,6 +23,10 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
 /**
  * A {@link Task} container that can contain one or two tasks, depending on if the two tasks
  * are represented as an app-pair in the recents task list.
@@ -62,6 +66,17 @@
     }
 
     /**
+     * Returns a List of all the Tasks in this GroupTask
+     */
+    public List<Task> getTasks() {
+        if (task2 == null) {
+            return Collections.singletonList(task1);
+        } else {
+            return Arrays.asList(task1, task2);
+        }
+    }
+
+    /**
      * Create a copy of this instance
      */
     public GroupTask copy() {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4e5d646..8ee7dbc 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1715,7 +1715,7 @@
         return super.isPageScrollsInitialized() && mLoadPlanEverApplied;
     }
 
-    protected void applyLoadPlan(ArrayList<GroupTask> taskGroups) {
+    protected void applyLoadPlan(List<GroupTask> taskGroups) {
         if (mPendingAnimation != null) {
             mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups));
             return;
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt
new file mode 100644
index 0000000..eaeb513
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.GroupTask
+import java.util.function.Consumer
+
+class FakeRecentTasksDataSource : RecentTasksDataSource {
+    var taskList: List<GroupTask> = listOf()
+
+    override fun getTasks(callback: Consumer<List<GroupTask>>?): Int {
+        callback?.accept(taskList)
+        return 0
+    }
+
+    fun seedTasks(tasks: List<GroupTask>) {
+        taskList = tasks
+    }
+}
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
new file mode 100644
index 0000000..b66b735
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.graphics.Bitmap
+import com.android.launcher3.util.CancellableTask
+import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import java.util.function.Consumer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
+
+    val taskIdToBitmap: Map<Int, Bitmap> = (0..10).associateWith { mock() }
+    val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
+    var shouldLoadSynchronously: Boolean = true
+
+    /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
+    override fun updateThumbnailInBackground(
+        task: Task,
+        callback: Consumer<ThumbnailData>
+    ): CancellableTask<ThumbnailData>? {
+        val thumbnailData = mock<ThumbnailData>()
+        whenever(thumbnailData.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
+        val wrappedCallback = {
+            task.thumbnail = thumbnailData
+            callback.accept(thumbnailData)
+        }
+        if (shouldLoadSynchronously) {
+            wrappedCallback()
+        } else {
+            taskIdToUpdatingTask[task.key.id] = wrappedCallback
+        }
+        return null
+    }
+}
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
new file mode 100644
index 0000000..c28a85a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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 com.android.quickstep.TaskIconCache
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.kotlin.mock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class TasksRepositoryTest {
+    private val tasks = (0..5).map(::createTaskWithId)
+    private val defaultTaskList =
+        listOf(
+            GroupTask(tasks[0]),
+            GroupTask(tasks[1], tasks[2], null),
+            DesktopTask(tasks.subList(3, 6))
+        )
+    private val recentsModel = FakeRecentTasksDataSource()
+    private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
+    private val taskIconCache = mock<TaskIconCache>()
+
+    private val systemUnderTest =
+        TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache)
+
+    @Test
+    fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+
+        assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks)
+    }
+
+    @Test
+    fun getTaskDataByIdReturnsSpecificTask() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2])
+    }
+
+    @Test
+    fun setVisibleTasksPopulatesThumbnails() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1]
+        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // .drop(1) to ignore initial null content before from thumbnail was loaded.
+        assertThat(systemUnderTest.getTaskDataById(1).drop(1).first()!!.thumbnail!!.thumbnail)
+            .isEqualTo(bitmap1)
+        assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
+            .isEqualTo(bitmap2)
+    }
+
+    @Test
+    fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // .drop(1) to ignore initial null content before from thumbnail was loaded.
+        assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail)
+            .isEqualTo(bitmap2)
+
+        // Prevent new loading of Bitmaps
+        taskThumbnailDataSource.shouldLoadSynchronously = false
+        systemUnderTest.setVisibleTasks(listOf(2, 3))
+
+        assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
+            .isEqualTo(bitmap2)
+    }
+
+    @Test
+    fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // .drop(1) to ignore initial null content before from thumbnail was loaded.
+        assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail)
+            .isEqualTo(bitmap2)
+
+        // Prevent new loading of Bitmaps
+        taskThumbnailDataSource.shouldLoadSynchronously = false
+        systemUnderTest.setVisibleTasks(listOf(0, 1))
+
+        assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
+    }
+
+    @Test
+    fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = runTest {
+        // Setup fakes
+        recentsModel.seedTasks(defaultTaskList)
+        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
+        taskThumbnailDataSource.shouldLoadSynchronously = false
+
+        // Setup TasksRepository
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // Assert there is no bitmap in first emission
+        val taskFlow = systemUnderTest.getTaskDataById(2)
+        val taskFlowValuesList = mutableListOf<Task?>()
+        backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
+            taskFlow.toList(taskFlowValuesList)
+        }
+        assertThat(taskFlowValuesList[0]!!.thumbnail).isNull()
+
+        // Simulate bitmap loading after first emission
+        taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke()
+
+        // Check for second emission
+        assertThat(taskFlowValuesList[1]!!.thumbnail!!.thumbnail).isEqualTo(bitmap2)
+    }
+
+    private fun createTaskWithId(taskId: Int) =
+        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000))
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 0de5f19..aa08ca4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -31,7 +31,6 @@
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
-import com.android.launcher3.statemanager.StatefulActivity
 import com.android.launcher3.util.ComponentKey
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.quickstep.RecentsModel
@@ -121,7 +120,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonMatchingComponent),
                         false /* findExactPairMatch */,
@@ -174,7 +173,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent),
                         false /* findExactPairMatch */,
@@ -215,7 +214,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonPrimaryUserComponent),
                         false /* findExactPairMatch */,
@@ -271,7 +270,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonPrimaryUserComponent),
                         false /* findExactPairMatch */,
@@ -324,7 +323,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent),
                         false /* findExactPairMatch */,
@@ -378,7 +377,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonMatchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
@@ -431,7 +430,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
@@ -497,7 +496,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
@@ -549,7 +548,7 @@
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
-            argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+            argumentCaptor<Consumer<List<GroupTask>>> {
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent2, matchingComponent),
                         true /* findExactPairMatch */,