Add snapshot state to TaskThumbnailView and ViewModel

Bug: 334825222
Test: TaskThumbnailViewModelTest
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Change-Id: I829c346861a6f437b2c53dadb3a7f98084d3c962
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
new file mode 100644
index 0000000..c1eef0b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.systemui.shared.recents.model.Task
+import kotlinx.coroutines.flow.Flow
+
+interface RecentTasksRepository {
+    /** Gets all the recent tasks, refreshing from data sources if [forceRefresh] is true. */
+    fun getAllTaskData(forceRefresh: Boolean = false): Flow<List<Task>>
+
+    /**
+     * Gets the data associated with a task that has id [taskId]. Flow will settle on null if the
+     * task was not found.
+     */
+    fun getTaskDataById(taskId: Int): Flow<Task?>
+
+    /**
+     * 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>)
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index ad8ae20..b21a1b4 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -38,7 +38,7 @@
     private val recentsModel: RecentTasksDataSource,
     private val taskThumbnailDataSource: TaskThumbnailDataSource,
     private val taskIconCache: TaskIconCache,
-) {
+) : RecentTasksRepository {
     private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
     private val _taskData =
         groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
@@ -53,17 +53,17 @@
             tasks
         }
 
-    fun getAllTaskData(forceRefresh: Boolean = false): Flow<List<Task>> {
+    override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
         if (forceRefresh) {
             recentsModel.getTasks { groupedTaskData.value = it }
         }
         return taskData
     }
 
-    fun getTaskDataById(taskId: Int): Flow<Task?> =
+    override fun getTaskDataById(taskId: Int): Flow<Task?> =
         taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
 
-    fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         this.visibleTaskIds.value = visibleTaskIdList.toSet()
     }
 
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 0843ae3..40f9b28 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -16,11 +16,19 @@
 
 package com.android.quickstep.task.thumbnail
 
-import com.android.systemui.shared.recents.model.Task
+import android.graphics.Bitmap
+import android.graphics.Rect
+import androidx.annotation.ColorInt
 
 sealed class TaskThumbnailUiState {
     data object Uninitialized : TaskThumbnailUiState()
     data object LiveTile : TaskThumbnailUiState()
+    data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
+    data class Snapshot(
+        val bitmap: Bitmap,
+        val drawnRect: Rect,
+        @ColorInt val backgroundColor: Int
+    ) : TaskThumbnailUiState()
 }
 
-data class TaskThumbnail(val task: Task, val isRunning: Boolean)
+data class TaskThumbnail(val taskId: Int, val isRunning: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 8762976..2836c89 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -19,15 +19,20 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Outline
 import android.graphics.Paint
 import android.graphics.PorterDuff
 import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
 import android.util.AttributeSet
 import android.view.View
 import android.view.ViewOutlineProvider
+import androidx.annotation.ColorInt
 import com.android.launcher3.Utilities
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.util.TaskCornerRadius
 import com.android.quickstep.views.RecentsView
@@ -42,17 +47,26 @@
     //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
     //  This is using a lazy for now because the dependencies cannot be obtained without DI.
     val viewModel by lazy {
-        TaskThumbnailViewModel(
+        val recentsView =
             RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
                 .getOverviewPanel<RecentsView<*, *>>()
-                .mRecentsViewData,
-            (parent as TaskView).taskViewData
+        TaskThumbnailViewModel(
+            recentsView.mRecentsViewData,
+            (parent as TaskView).taskViewData,
+            recentsView.mTasksRepository,
         )
     }
 
     private var uiState: TaskThumbnailUiState = Uninitialized
     private var inheritedScale: Float = 1f
 
+    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+    private val _measuredBounds = Rect()
+    private val measuredBounds: Rect
+        get() {
+            _measuredBounds.set(0, 0, measuredWidth, measuredHeight)
+            return _measuredBounds
+        }
     private var cornerRadius: Float = TaskCornerRadius.get(context)
     private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
 
@@ -85,24 +99,25 @@
         outlineProvider =
             object : ViewOutlineProvider() {
                 override fun getOutline(view: View, outline: Outline) {
-                    outline.setRoundRect(
-                        0,
-                        0,
-                        view.measuredWidth,
-                        view.measuredHeight,
-                        getCurrentCornerRadius()
-                    )
+                    outline.setRoundRect(measuredBounds, getCurrentCornerRadius())
                 }
             }
     }
 
     override fun onDraw(canvas: Canvas) {
-        when (uiState) {
-            is Uninitialized -> {}
+        when (val uiStateVal = uiState) {
+            is Uninitialized -> drawBackgroundOnly(canvas, Color.BLACK)
             is LiveTile -> drawTransparentUiState(canvas)
+            is Snapshot -> drawSnapshotState(canvas, uiStateVal)
+            is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor)
         }
     }
 
+    private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) {
+        backgroundPaint.color = backgroundColor
+        canvas.drawRect(measuredBounds, backgroundPaint)
+    }
+
     override fun onConfigurationChanged(newConfig: Configuration?) {
         super.onConfigurationChanged(newConfig)
 
@@ -112,7 +127,12 @@
     }
 
     private fun drawTransparentUiState(canvas: Canvas) {
-        canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), CLEAR_PAINT)
+        canvas.drawRect(measuredBounds, CLEAR_PAINT)
+    }
+
+    private fun drawSnapshotState(canvas: Canvas, snapshot: Snapshot) {
+        drawBackgroundOnly(canvas, snapshot.backgroundColor)
+        canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null)
     }
 
     private fun getCurrentCornerRadius() =
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
index 71bc865..4511ea7 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
@@ -16,32 +16,76 @@
 
 package com.android.quickstep.task.thumbnail
 
+import android.annotation.ColorInt
+import android.graphics.Rect
+import androidx.core.graphics.ColorUtils
+import com.android.quickstep.recents.data.RecentTasksRepository
 import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.systemui.shared.recents.model.Task
+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.flowOf
 import kotlinx.coroutines.flow.map
 
-class TaskThumbnailViewModel(recentsViewData: RecentsViewData, taskViewData: TaskViewData) {
-    private val task = MutableStateFlow<TaskThumbnail?>(null)
+@OptIn(ExperimentalCoroutinesApi::class)
+class TaskThumbnailViewModel(
+    recentsViewData: RecentsViewData,
+    taskViewData: TaskViewData,
+    private val tasksRepository: RecentTasksRepository,
+) {
+    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
+    private var boundTaskIsRunning = false
 
     val recentsFullscreenProgress = recentsViewData.fullscreenProgress
     val inheritedScale =
         combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
             recentsScale * taskScale
         }
-    val uiState =
-        task.map { taskVal ->
-            when {
-                taskVal == null -> Uninitialized
-                taskVal.isRunning -> LiveTile
-                else -> Uninitialized
+    val uiState: Flow<TaskThumbnailUiState> =
+        task
+            .flatMapLatest { taskFlow ->
+                taskFlow.map { taskVal ->
+                    when {
+                        taskVal == null -> Uninitialized
+                        boundTaskIsRunning -> LiveTile
+                        isBackgroundOnly(taskVal) ->
+                            BackgroundOnly(taskVal.colorBackground.removeAlpha())
+                        isSnapshotState(taskVal) -> {
+                            val bitmap = taskVal.thumbnail?.thumbnail!!
+                            Snapshot(
+                                bitmap,
+                                Rect(0, 0, bitmap.width, bitmap.height),
+                                taskVal.colorBackground.removeAlpha()
+                            )
+                        }
+                        else -> Uninitialized
+                    }
+                }
             }
-        }
+            .distinctUntilChanged()
 
-    fun bind(task: TaskThumbnail) {
-        this.task.value = task
+    fun bind(taskThumbnail: TaskThumbnail) {
+        boundTaskIsRunning = taskThumbnail.isRunning
+        task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
     }
+
+    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
+
+    private fun isSnapshotState(task: Task): Boolean {
+        val thumbnailPresent = task.thumbnail?.thumbnail != null
+        val taskLocked = task.isLocked
+
+        return thumbnailPresent && !taskLocked
+    }
+
+    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5eee64d..4804e56 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -188,6 +188,7 @@
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
+import com.android.quickstep.recents.data.TasksRepository;
 import com.android.quickstep.recents.viewmodel.RecentsViewData;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -305,6 +306,7 @@
     public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f;
     public static final VibrationEffect SCROLL_VIBRATION_FALLBACK =
             VibrationConstants.EFFECT_TEXTURE_TICK;
+    public static final int UNBOUND_TASK_VIEW_ID = -1;
 
     /**
      * Can be used to tint the color of the RecentsView to simulate a scrim that can views
@@ -456,6 +458,7 @@
     private static final float FOREGROUND_SCRIM_TINT = 0.32f;
 
     public final RecentsViewData mRecentsViewData = new RecentsViewData();
+    public final TasksRepository mTasksRepository;
 
     protected final RecentsOrientedState mOrientationState;
     protected final BaseContainerInterface<STATE_TYPE, CONTAINER_TYPE> mSizeStrategy;
@@ -800,6 +803,12 @@
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
+        if (enableRefactorTaskThumbnail()) {
+            mTasksRepository = new TasksRepository(
+                    mModel, mModel.getThumbnailCache(), mModel.getIconCache());
+        } else {
+            mTasksRepository = null;
+        }
 
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
@@ -1165,7 +1174,6 @@
             } else {
                 mTaskViewPool.recycle(taskView);
             }
-            taskView.setTaskViewId(-1);
             mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
         }
     }
@@ -2362,6 +2370,8 @@
             upper = Math.min(centerPageIndex + 2, numChildren - 1);
         }
 
+        List<Integer> visibleTaskIds = new ArrayList<>();
+
         // Update the task data for the in/visible children
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
@@ -2381,6 +2391,10 @@
                 List<Task> tasksToUpdate = containers.stream()
                         .map(TaskContainer::getTask)
                         .collect(Collectors.toCollection(ArrayList::new));
+                if (enableRefactorTaskThumbnail()) {
+                    visibleTaskIds.addAll(
+                            tasksToUpdate.stream().map((task) -> task.key.id).toList());
+                }
                 if (mTmpRunningTasks != null) {
                     for (Task t : mTmpRunningTasks) {
                         // Skip loading if this is the task that we are animating into
@@ -2416,6 +2430,9 @@
                 }
             }
         }
+        if (enableRefactorTaskThumbnail()) {
+            mTasksRepository.setVisibleTasks(visibleTaskIds);
+        }
     }
 
     /**
@@ -2602,6 +2619,9 @@
         if (!mModel.isTaskListValid(mTaskListChangeId)) {
             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
                     .getFilter(mFilterState.getPackageNameToFilter()));
+            if (enableRefactorTaskThumbnail()) {
+                mTasksRepository.getAllTaskData(/* forceRefresh = */ true);
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 05b9d40..4045ad7 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -93,6 +93,7 @@
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.quickstep.util.TaskCornerRadius
 import com.android.quickstep.util.TaskRemovedDuringLaunchListener
+import com.android.quickstep.views.RecentsView.UNBOUND_TASK_VIEW_ID
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.system.ActivityManagerWrapper
@@ -235,7 +236,7 @@
         protected set
     lateinit var orientedState: RecentsOrientedState
 
-    var taskViewId = -1
+    var taskViewId = UNBOUND_TASK_VIEW_ID
     var isEndQuickSwitchCuj = false
 
     // Various animation progress variables.
@@ -502,7 +503,6 @@
         resetPersistentViewTransforms()
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
-        // TODO(b/334825222): Implement thumbnail/snapshot for the new [TaskThumbnailView].
         if (enableRefactorTaskThumbnail()) {
             notifyIsRunningTaskUpdated()
         } else {
@@ -511,6 +511,7 @@
         setOverlayEnabled(false)
         onTaskListVisibilityChanged(false)
         borderEnabled = false
+        taskViewId = UNBOUND_TASK_VIEW_ID
     }
 
     // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
@@ -780,22 +781,19 @@
         val recentsModel = RecentsModel.INSTANCE.get(context)
         // These calls are no-ops if the data is already loaded, try and load the high
         // resolution thumbnail if the state permits
-        if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
-            if (!enableRefactorTaskThumbnail()) {
-                // TODO(b/334825222) add thumbnail state
-                taskContainers.forEach {
-                    if (visible) {
-                        recentsModel.thumbnailCache
-                            .updateThumbnailInBackground(it.task) { thumbnailData ->
-                                it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
-                            }
-                            ?.also { request -> pendingThumbnailLoadRequests.add(request) }
-                    } else {
-                        it.thumbnailViewDeprecated.setThumbnail(null, null)
-                        // Reset the task thumbnail reference as well (it will be fetched from the
-                        // cache or reloaded next time we need it)
-                        it.task.thumbnail = null
-                    }
+        if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL) && !enableRefactorTaskThumbnail()) {
+            taskContainers.forEach {
+                if (visible) {
+                    recentsModel.thumbnailCache
+                        .updateThumbnailInBackground(it.task) { thumbnailData ->
+                            it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
+                        }
+                        ?.also { request -> pendingThumbnailLoadRequests.add(request) }
+                } else {
+                    it.thumbnailViewDeprecated.setThumbnail(null, null)
+                    // Reset the task thumbnail reference as well (it will be fetched from the
+                    // cache or reloaded next time we need it)
+                    it.task.thumbnail = null
                 }
             }
         }
@@ -861,7 +859,7 @@
 
     open fun refreshThumbnails(thumbnailDatas: HashMap<Int, ThumbnailData?>?) {
         if (enableRefactorTaskThumbnail()) {
-            // TODO(b/334825222) add thumbnail logic
+            // TODO(b/342560598) add thumbnail logic
             return
         }
 
@@ -1591,7 +1589,9 @@
         // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
         //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
         fun bindThumbnailView() {
-            thumbnailView?.viewModel?.bind(TaskThumbnail(task, isRunningTask))
+            // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
+            //  this should be decided inside TaskThumbnailViewModel.
+            thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, isRunningTask))
         }
     }
 
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
new file mode 100644
index 0000000..e160627
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+
+class FakeTasksRepository : RecentTasksRepository {
+    private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
+    private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
+    private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
+
+    override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks
+
+    override fun getTaskDataById(taskId: Int): Flow<Task?> =
+        getAllTaskData().map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+
+    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+        visibleTasks.value = visibleTaskIdList
+        tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } }
+    }
+
+    fun seedTasks(tasks: List<Task>) {
+        this.tasks.value = tasks
+    }
+
+    fun seedThumbnailData(thumbnailDataMap: Map<Int, ThumbnailData>) {
+        this.thumbnailDataMap = thumbnailDataMap
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index efd7bec..3b8754c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -16,33 +16,51 @@
 
 package com.android.quickstep.task.thumbnail
 
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Rect
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeTasksRepository
 import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskViewData
 import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 class TaskThumbnailViewModelTest {
     private val recentsViewData = RecentsViewData()
     private val taskViewData = TaskViewData()
-    private val systemUnderTest = TaskThumbnailViewModel(recentsViewData, taskViewData)
+    private val tasksRepository = FakeTasksRepository()
+    private val systemUnderTest =
+        TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository)
+
+    private val tasks = (0..5).map(::createTaskWithId)
 
     @Test
     fun initialStateIsUninitialized() = runTest {
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.Uninitialized)
+        assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
     }
 
     @Test
     fun bindRunningTask_thenStateIs_LiveTile() = runTest {
-        val taskThumbnail = TaskThumbnail(Task(), isRunning = true)
+        tasksRepository.seedTasks(tasks)
+        val taskThumbnail = TaskThumbnail(taskId = 1, isRunning = true)
         systemUnderTest.bind(taskThumbnail)
 
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.LiveTile)
+        assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
     }
 
     @Test
@@ -65,15 +83,96 @@
     }
 
     @Test
-    fun bindRunningTaskThenStoppedTask_thenStateIs_Uninitialized() = runTest {
-        // TODO(b/334825222): Change the expectation here when snapshot state is implemented
-        val task = Task()
-        val runningTask = TaskThumbnail(task, isRunning = true)
-        val stoppedTask = TaskThumbnail(task, isRunning = false)
-        systemUnderTest.bind(runningTask)
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.LiveTile)
+    fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
+        runTest {
+            tasksRepository.seedTasks(tasks)
+            val runningTask = TaskThumbnail(taskId = 1, isRunning = true)
+            val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
+            systemUnderTest.bind(runningTask)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+
+            systemUnderTest.bind(stoppedTask)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
+        tasksRepository.seedTasks(tasks)
+        val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
 
         systemUnderTest.bind(stoppedTask)
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.Uninitialized)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+    }
+
+    @Test
+    fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest {
+        tasksRepository.seedThumbnailData(mapOf(2 to createThumbnailData()))
+        tasks[2].isLocked = true
+        tasksRepository.seedTasks(tasks)
+        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+    }
+
+    @Test
+    fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest {
+        val expectedThumbnailData = createThumbnailData()
+        tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+        tasksRepository.seedTasks(tasks)
+        tasksRepository.setVisibleTasks(listOf(2))
+        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(
+                Snapshot(
+                    backgroundColor = Color.rgb(2, 2, 2),
+                    bitmap = expectedThumbnailData.thumbnail!!,
+                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
+                )
+            )
+    }
+
+    @Test
+    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest {
+        val expectedThumbnailData = createThumbnailData()
+        tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+        tasksRepository.seedTasks(tasks)
+        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        tasksRepository.setVisibleTasks(listOf(2))
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(
+                Snapshot(
+                    backgroundColor = Color.rgb(2, 2, 2),
+                    bitmap = expectedThumbnailData.thumbnail!!,
+                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
+                )
+            )
+    }
+
+    private fun createTaskWithId(taskId: Int) =
+        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+        }
+
+    private fun createThumbnailData(): ThumbnailData {
+        val bitmap = mock<Bitmap>()
+        whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
+        whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
+
+        return ThumbnailData(thumbnail = bitmap)
+    }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
     }
 }