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
}
}