Merge "Refactor: Remove TaskThumbnailViewModel and introduce TaskViewModel" into main

This commit is contained in:
Jordan Silva
2025-01-27 03:09:37 -08:00
committed by Android (Google) Code Review
15 changed files with 500 additions and 435 deletions
@@ -26,6 +26,7 @@ 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.domain.usecase.GetTaskUseCase
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
@@ -181,8 +182,6 @@ class RecentsDependencies private constructor(private val appContext: Context) {
taskContainerData = inject(scopeId),
dispatcherProvider = inject(),
getThumbnailPositionUseCase = inject(),
tasksRepository = inject(),
deviceProfileRepository = inject(),
splashAlphaUseCase = inject(scopeId),
)
TaskOverlayViewModel::class.java -> {
@@ -195,6 +194,7 @@ class RecentsDependencies private constructor(private val appContext: Context) {
dispatcherProvider = inject(),
)
}
GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject())
GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
SysUiStatusNavFlagsUseCase::class.java ->
SysUiStatusNavFlagsUseCase(taskRepository = inject())
@@ -38,7 +38,7 @@ import com.android.systemui.shared.recents.model.ThumbnailData
*/
data class TaskModel(
val id: TaskId,
val title: String,
val title: String?,
val titleDescription: String?,
val icon: Drawable?,
val thumbnail: ThumbnailData?,
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2025 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.ui.mapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
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.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
object TaskUiStateMapper {
/**
* Converts a [TaskData] object into a [TaskThumbnailUiState] for display in the UI.
*
* This function handles different types of [TaskData] and determines the appropriate UI state
* based on the data and provided flags.
*
* @param taskData The [TaskData] to convert. Can be null or a specific subclass.
* @param isLiveTile A flag indicating whether the task data represents live tile.
* @param hasHeader A flag indicating whether the UI should display a header.
* @return A [TaskThumbnailUiState] representing the UI state for the given task data.
*/
fun toTaskThumbnailUiState(
taskData: TaskData?,
isLiveTile: Boolean,
hasHeader: Boolean,
): TaskThumbnailUiState =
when {
taskData !is TaskData.Data -> Uninitialized
isLiveTile -> createLiveTileState(taskData, hasHeader)
isBackgroundOnly(taskData) -> BackgroundOnly(taskData.backgroundColor)
isSnapshotSplash(taskData) ->
SnapshotSplash(createSnapshotState(taskData, hasHeader), taskData.icon)
else -> Uninitialized
}
private fun createSnapshotState(taskData: TaskData.Data, hasHeader: Boolean): Snapshot =
if (canHeaderBeCreated(taskData, hasHeader)) {
Snapshot.WithHeader(
taskData.thumbnailData?.thumbnail!!,
taskData.thumbnailData.rotation,
taskData.backgroundColor,
ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!),
)
} else {
Snapshot.WithoutHeader(
taskData.thumbnailData?.thumbnail!!,
taskData.thumbnailData.rotation,
taskData.backgroundColor,
)
}
private fun isBackgroundOnly(taskData: TaskData.Data) =
taskData.isLocked || taskData.thumbnailData == null
private fun isSnapshotSplash(taskData: TaskData.Data) =
taskData.thumbnailData?.thumbnail != null && !taskData.isLocked
private fun canHeaderBeCreated(taskData: TaskData.Data, hasHeader: Boolean) =
hasHeader && taskData.icon != null && taskData.titleDescription != null
private fun createLiveTileState(taskData: TaskData.Data, hasHeader: Boolean) =
if (canHeaderBeCreated(taskData, hasHeader)) {
// TODO(http://b/353965691): figure out what to do when `icon` or `titleDescription` is
// null.
LiveTile.WithHeader(ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!))
} else LiveTile.WithoutHeader
}
@@ -30,28 +30,36 @@ import com.android.systemui.shared.recents.model.ThumbnailData
* @property isLiveTile Indicates whether this data is intended for a live tile. If `true`, the
* running app will be displayed instead of the thumbnail.
*/
data class TaskTileUiState(val tasks: List<TaskData>, val isLiveTile: Boolean)
data class TaskTileUiState(
val tasks: List<TaskData>,
val isLiveTile: Boolean,
val hasHeader: Boolean,
)
sealed class TaskData {
abstract val taskId: Int
sealed interface TaskData {
/** When no data was found for the TaskId provided */
data class NoData(val taskId: Int) : TaskData
data class NoData(override val taskId: Int) : TaskData()
/**
* This class provides UI information related to a Task (App) to be displayed within a TaskView.
*
* @property taskId Identifier of the task
* @property title App title
* @property titleDescription App content description
* @property icon App icon
* @property thumbnailData Information related to the last snapshot retrieved from the app
* @property backgroundColor The background color of the task.
* @property isLocked Indicates whether the task is locked or not.
*/
data class Data(
val taskId: Int,
val title: String,
override val taskId: Int,
val title: String?,
val titleDescription: String?,
val icon: Drawable?,
val thumbnailData: ThumbnailData?,
val backgroundColor: Int,
val isLocked: Boolean,
) : TaskData
) : TaskData()
}
@@ -16,12 +16,15 @@
package com.android.quickstep.recents.ui.viewmodel
import android.annotation.ColorInt
import android.util.Log
import androidx.core.graphics.ColorUtils
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.domain.model.TaskId
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,6 +40,7 @@ import kotlinx.coroutines.flow.map
*/
@OptIn(ExperimentalCoroutinesApi::class)
class TaskViewModel(
private val taskViewType: TaskViewType,
recentsViewData: RecentsViewData,
private val getTaskUseCase: GetTaskUseCase,
dispatcherProvider: DispatcherProvider,
@@ -62,7 +66,14 @@ class TaskViewModel(
::mapToUiState,
)
}
.combine(isLiveTile) { tasks, isLiveTile -> TaskTileUiState(tasks, isLiveTile) }
.combine(isLiveTile) { tasks, isLiveTile ->
TaskTileUiState(
tasks = tasks,
isLiveTile = isLiveTile,
hasHeader = taskViewType == TaskViewType.DESKTOP,
)
}
.distinctUntilChanged()
.flowOn(dispatcherProvider.background)
fun bind(vararg taskId: TaskId) {
@@ -78,13 +89,16 @@ class TaskViewModel(
TaskData.Data(
taskId = taskId,
title = result.title,
titleDescription = result.titleDescription,
icon = result.icon,
thumbnailData = result.thumbnail,
backgroundColor = result.backgroundColor,
backgroundColor = result.backgroundColor.removeAlpha(),
isLocked = result.isLocked,
)
} ?: TaskData.NoData(taskId)
@ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
private companion object {
const val TAG = "TaskViewModel"
}
@@ -108,21 +108,6 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
viewData = RecentsDependencies.get(this)
updateViewDataValues()
viewModel = RecentsDependencies.get(this)
viewModel.uiState
.dropWhile { it == Uninitialized }
.flowOn(dispatcherProvider.background)
.onEach { viewModelUiState ->
Log.d(TAG, "viewModelUiState changed from: $uiState to: $viewModelUiState")
uiState = viewModelUiState
resetViews()
when (viewModelUiState) {
is Uninitialized -> {}
is LiveTile -> drawLiveWindow(viewModelUiState)
is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
}
}
.launchIn(viewAttachedScope)
viewModel.dimProgress
.dropWhile { it == 0f }
.flowOn(dispatcherProvider.background)
@@ -166,6 +151,19 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
}
}
fun setState(state: TaskThumbnailUiState) {
Log.d(TAG, "viewModelUiState changed from: $uiState to: $state")
if (uiState == state) return
uiState = state
resetViews()
when (state) {
is Uninitialized -> {}
is LiveTile -> drawLiveWindow(state)
is SnapshotSplash -> drawSnapshotSplash(state)
is BackgroundOnly -> drawBackground(state.backgroundColor)
}
}
private fun updateViewDataValues() {
viewData.width.value = width
viewData.height.value = height
@@ -238,10 +236,6 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
}
private companion object {
const val TAG = "TaskThumbnailView"
}
private fun maybeCreateHeader() {
if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) {
taskThumbnailViewHeader =
@@ -251,4 +245,8 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
addView(taskThumbnailViewHeader)
}
}
private companion object {
const val TAG = "TaskThumbnailView"
}
}
@@ -17,7 +17,6 @@
package com.android.quickstep.task.viewmodel
import android.graphics.Matrix
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
import kotlinx.coroutines.flow.Flow
/** ViewModel for representing TaskThumbnails */
@@ -28,9 +27,6 @@ interface TaskThumbnailViewModel {
/** Provides the alpha of the splash icon */
val splashAlpha: Flow<Float>
/** Provides the UiState by which the task thumbnail can be represented */
val uiState: Flow<TaskThumbnailUiState>
/** Attaches this ViewModel to a specific task id for it to provide data from. */
fun bind(taskId: Int)
@@ -16,50 +16,31 @@
package com.android.quickstep.task.viewmodel
import android.annotation.ColorInt
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.graphics.Matrix
import android.util.Log
import androidx.core.graphics.ColorUtils
import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
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.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.systemui.shared.recents.model.Task
import kotlin.math.max
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.flowOn
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalCoroutinesApi::class)
class TaskThumbnailViewModelImpl(
recentsViewData: RecentsViewData,
taskContainerData: TaskContainerData,
dispatcherProvider: DispatcherProvider,
private val tasksRepository: RecentTasksRepository,
private val deviceProfileRepository: RecentsDeviceProfileRepository,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
private val splashAlphaUseCase: SplashAlphaUseCase,
) : TaskThumbnailViewModel {
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
private val splashProgress = MutableStateFlow(flowOf(0f))
private var taskId: Int = INVALID_TASK_ID
@@ -74,42 +55,9 @@ class TaskThumbnailViewModelImpl(
override val splashAlpha =
splashProgress.flatMapLatest { it }.flowOn(dispatcherProvider.background)
private val isLiveTile =
combine(
task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
recentsViewData.runningTaskIds,
recentsViewData.runningTaskShowScreenshot,
) { taskId, runningTaskIds, runningTaskShowScreenshot ->
runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
}
.distinctUntilChanged()
override val uiState: Flow<TaskThumbnailUiState> =
combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning ->
// TODO(b/369339561) This log is firing a lot. Reduce emissions from TasksRepository
// then re-enable this log.
// Log.d(
// TAG,
// "Received task and / or live tile update. taskVal: $taskVal"
// + " isRunning: $isRunning.",
// )
when {
taskVal == null -> Uninitialized
isRunning -> createLiveTileState(taskVal)
isBackgroundOnly(taskVal) ->
BackgroundOnly(taskVal.colorBackground.removeAlpha())
isSnapshotSplashState(taskVal) ->
SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
else -> Uninitialized
}
}
.distinctUntilChanged()
.flowOn(dispatcherProvider.background)
override fun bind(taskId: Int) {
Log.d(TAG, "bind taskId: $taskId")
this.taskId = taskId
task.value = tasksRepository.getTaskDataById(taskId)
splashProgress.value = splashAlphaUseCase.execute(taskId)
}
@@ -122,62 +70,6 @@ class TaskThumbnailViewModelImpl(
is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
}
private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
private fun isSnapshotSplashState(task: Task): Boolean {
val thumbnailPresent = task.thumbnail?.thumbnail != null
val taskLocked = task.isLocked
return thumbnailPresent && !taskLocked
}
private fun createSnapshotState(task: Task): Snapshot {
val thumbnailData = task.thumbnail
val bitmap = thumbnailData?.thumbnail!!
var thumbnailHeader = maybeCreateHeader(task)
return if (thumbnailHeader != null)
Snapshot.WithHeader(
bitmap,
thumbnailData.rotation,
task.colorBackground.removeAlpha(),
thumbnailHeader,
)
else
Snapshot.WithoutHeader(
bitmap,
thumbnailData.rotation,
task.colorBackground.removeAlpha(),
)
}
private fun shouldHaveThumbnailHeader(task: Task): Boolean {
return deviceProfileRepository.getRecentsDeviceProfile().canEnterDesktopMode &&
enableDesktopExplodedView() &&
task.key.windowingMode == WINDOWING_MODE_FREEFORM
}
private fun maybeCreateHeader(task: Task): ThumbnailHeader? {
// Header is only needed when this task is a desktop task and Overivew exploded view is
// enabled.
if (!shouldHaveThumbnailHeader(task)) {
return null
}
// TODO(http://b/353965691): figure out what to do when `icon` or `titleDescription` is
// null.
val icon = task.icon ?: return null
val titleDescription = task.titleDescription ?: return null
return ThumbnailHeader(icon, titleDescription)
}
private fun createLiveTileState(task: Task): LiveTile {
val thumbnailHeader = maybeCreateHeader(task)
return if (thumbnailHeader != null) LiveTile.WithHeader(thumbnailHeader)
else LiveTile.WithoutHeader
}
@ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
private companion object {
const val MAX_SCRIM_ALPHA = 0.4f
const val TAG = "TaskThumbnailViewModel"
@@ -28,6 +28,8 @@ import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.getScope
import com.android.quickstep.recents.di.inject
import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.task.viewmodel.TaskContainerData
@@ -163,4 +165,8 @@ class TaskContainer(
digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
overlay.addChildForAccessibility(outChildren)
}
fun setState(state: TaskData?, liveTile: Boolean, hasHeader: Boolean) {
thumbnailView.setState(TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader))
}
}
@@ -70,6 +70,7 @@ import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
import com.android.launcher3.util.TraceHelper
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.launcher3.util.ViewPool
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.launcher3.util.rects.set
import com.android.quickstep.FullscreenDrawParams
import com.android.quickstep.RecentsModel
@@ -77,6 +78,11 @@ import com.android.quickstep.RemoteAnimationTargets
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.TaskViewUtils
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.inject
import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.util.BorderAnimator
@@ -88,6 +94,12 @@ 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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/** A task in the Recents view. */
open class TaskView
@@ -448,6 +460,11 @@ constructor(
private val settledProgressDismiss =
settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_DISMISS)
private var viewModel: TaskViewModel? = null
private val dispatcherProvider: DispatcherProvider by RecentsDependencies.inject()
private val coroutineScope by lazy { CoroutineScope(SupervisorJob() + dispatcherProvider.main) }
private val coroutineJobs = mutableListOf<Job>()
/**
* Returns an animator of [settledProgressDismiss] that transition in with a built-in
* interpolator.
@@ -601,6 +618,8 @@ constructor(
override fun onRecycle() {
resetPersistentViewTransforms()
viewModel = null
attachAlpha = 1f
splitAlpha = 1f
// Clear any references to the thumbnail (it will be re-read either from the cache or the
@@ -715,6 +734,44 @@ constructor(
?.inflate()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (enableRefactorTaskThumbnail()) {
// The TaskView lifecycle is starts the ViewModel during onBind, and cleans it in
// onRecycle. So it should be initialized at this point. TaskView Lifecycle:
// `bind` -> `onBind` -> onAttachedToWindow() -> onDetachFromWindow -> onRecycle
coroutineJobs +=
coroutineScope.launch {
viewModel!!.state.collectLatest(::updateTaskContainerState)
}
}
}
private fun updateTaskContainerState(state: TaskTileUiState) {
val mapOfTasks = state.tasks.associateBy { it.taskId }
taskContainers.forEach { container ->
container.setState(
state = mapOfTasks[container.task.key.id],
liveTile = state.isLiveTile,
hasHeader = type == TaskViewType.DESKTOP,
)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (enableRefactorTaskThumbnail()) {
// The jobs are being cancelled in the background thread. So we make a copy of the list
// to prevent cleaning a new job that might be added to this list during onAttach
// or another moment in the lifecycle.
val coroutineJobsToCancel = coroutineJobs.toList()
coroutineJobs.clear()
coroutineScope.launch(dispatcherProvider.background) {
coroutineJobsToCancel.forEach { it.cancel("TaskView detaching from window") }
}
}
}
/** Updates this task view to the given {@param task}. */
open fun bind(
task: Task,
@@ -738,6 +795,17 @@ constructor(
}
open fun onBind(orientedState: RecentsOrientedState) {
if (enableRefactorTaskThumbnail()) {
viewModel =
TaskViewModel(
taskViewType = type,
recentsViewData = RecentsDependencies.get(),
getTaskUseCase = RecentsDependencies.get(),
dispatcherProvider = RecentsDependencies.get(),
)
.apply { bind(*taskIds) }
}
taskContainers.forEach {
it.bind()
if (enableRefactorTaskThumbnail()) {
@@ -17,14 +17,12 @@
package com.android.quickstep.task.thumbnail
import android.graphics.Matrix
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import kotlinx.coroutines.flow.MutableStateFlow
class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
override val dimProgress = MutableStateFlow(0f)
override val splashAlpha = MutableStateFlow(0f)
override val uiState = MutableStateFlow<TaskThumbnailUiState>(Uninitialized)
override fun bind(taskId: Int) {
// no-op
@@ -60,36 +60,27 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
activity.actionBar?.hide()
val taskThumbnailView = createTaskThumbnailView(activity)
taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
taskThumbnailViewModel.uiState.value = Uninitialized
taskThumbnailView.setState(Uninitialized)
taskThumbnailView
}
}
@Test
fun taskThumbnailView_recyclesToUninitialized() {
screenshotRule.screenshotTest(
"taskThumbnailView_uninitialized",
viewProvider = { activity ->
activity.actionBar?.hide()
val taskThumbnailView = createTaskThumbnailView(activity)
taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
taskThumbnailView
},
checkView = { _, taskThumbnailView ->
// Call onRecycle() after View is attached (end of block above)
(taskThumbnailView as TaskThumbnailView).onRecycle()
false
},
)
screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
activity.actionBar?.hide()
val taskThumbnailView = createTaskThumbnailView(activity)
taskThumbnailView.setState(BackgroundOnly(Color.YELLOW))
taskThumbnailView.onRecycle()
taskThumbnailView
}
}
@Test
fun taskThumbnailView_backgroundOnly() {
screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity ->
activity.actionBar?.hide()
taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
createTaskThumbnailView(activity)
createTaskThumbnailView(activity).apply { setState(BackgroundOnly(Color.YELLOW)) }
}
}
@@ -0,0 +1,231 @@
/*
* Copyright (C) 2025 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.ui.mapper
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.platform.test.annotations.EnableFlags
import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.Flags
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TaskUiStateMapperTest {
@Test
fun taskData_isNull_returns_Uninitialized() {
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = null,
isLiveTile = false,
hasHeader = false,
)
assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized)
}
@Test
fun taskData_isLiveTile_returns_LiveTile() {
val inputs =
listOf(TASK_DATA, TASK_DATA.copy(thumbnailData = null), TASK_DATA.copy(isLocked = true))
inputs.forEach { input ->
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = input,
isLiveTile = true,
hasHeader = false,
)
assertThat(result).isEqualTo(LiveTile.WithoutHeader)
}
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
@Test
fun taskData_isLiveTileWithHeader_returns_LiveTileWithHeader() {
val inputs =
listOf(
TASK_DATA,
TASK_DATA.copy(thumbnailData = null),
TASK_DATA.copy(isLocked = true),
TASK_DATA.copy(title = null),
)
val expected =
LiveTile.WithHeader(header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION))
inputs.forEach { taskData ->
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = taskData,
isLiveTile = true,
hasHeader = true,
)
assertThat(result).isEqualTo(expected)
}
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
@Test
fun taskData_isLiveTileWithHeader_missingHeaderData_returns_LiveTileWithoutHeader() {
val inputs =
listOf(
TASK_DATA.copy(icon = null),
TASK_DATA.copy(titleDescription = null),
TASK_DATA.copy(icon = null, titleDescription = null),
)
inputs.forEach { taskData ->
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = taskData,
isLiveTile = true,
hasHeader = true,
)
assertThat(result).isEqualTo(LiveTile.WithoutHeader)
}
}
@Test
fun taskData_isStaticTile_returns_SnapshotSplash() {
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = TASK_DATA,
isLiveTile = false,
hasHeader = false,
)
val expected =
TaskThumbnailUiState.SnapshotSplash(
snapshot =
Snapshot.WithoutHeader(
backgroundColor = TASK_BACKGROUND_COLOR,
bitmap = TASK_THUMBNAIL,
thumbnailRotation = Surface.ROTATION_0,
),
splash = TASK_ICON,
)
assertThat(result).isEqualTo(expected)
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
@Test
fun taskData_isStaticTile_withHeader_returns_SnapshotSplashWithHeader() {
val inputs = listOf(TASK_DATA, TASK_DATA.copy(title = null))
val expected =
TaskThumbnailUiState.SnapshotSplash(
snapshot =
Snapshot.WithHeader(
backgroundColor = TASK_BACKGROUND_COLOR,
bitmap = TASK_THUMBNAIL,
thumbnailRotation = Surface.ROTATION_0,
header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION),
),
splash = TASK_ICON,
)
inputs.forEach { taskData ->
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = taskData,
isLiveTile = false,
hasHeader = true,
)
assertThat(result).isEqualTo(expected)
}
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
@Test
fun taskData_isStaticTile_missingHeaderData_returns_SnapshotSplashWithoutHeader() {
val inputs =
listOf(
TASK_DATA.copy(titleDescription = null, icon = null),
TASK_DATA.copy(titleDescription = null),
TASK_DATA.copy(icon = null),
)
val expected =
Snapshot.WithoutHeader(
backgroundColor = TASK_BACKGROUND_COLOR,
thumbnailRotation = Surface.ROTATION_0,
bitmap = TASK_THUMBNAIL,
)
inputs.forEach { taskData ->
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = taskData,
isLiveTile = false,
hasHeader = true,
)
assertThat(result).isInstanceOf(TaskThumbnailUiState.SnapshotSplash::class.java)
result as TaskThumbnailUiState.SnapshotSplash
assertThat(result.snapshot).isEqualTo(expected)
}
}
@Test
fun taskData_thumbnailIsNull_returns_BackgroundOnly() {
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = TASK_DATA.copy(thumbnailData = null),
isLiveTile = false,
hasHeader = false,
)
val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
assertThat(result).isEqualTo(expected)
}
@Test
fun taskData_isLocked_returns_BackgroundOnly() {
val result =
TaskUiStateMapper.toTaskThumbnailUiState(
taskData = TASK_DATA.copy(isLocked = true),
isLiveTile = false,
hasHeader = false,
)
val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
assertThat(result).isEqualTo(expected)
}
private companion object {
const val TASK_TITLE_DESCRIPTION = "Title Description 1"
val TASK_ICON = ShapeDrawable()
val TASK_THUMBNAIL = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
val TASK_THUMBNAIL_DATA =
ThumbnailData(thumbnail = TASK_THUMBNAIL, rotation = Surface.ROTATION_0)
val TASK_BACKGROUND_COLOR = Color.rgb(1, 2, 3)
val TASK_DATA =
TaskData.Data(
1,
title = "Task 1",
titleDescription = TASK_TITLE_DESCRIPTION,
icon = TASK_ICON,
thumbnailData = TASK_THUMBNAIL_DATA,
backgroundColor = TASK_BACKGROUND_COLOR,
isLocked = false,
)
}
}
@@ -23,6 +23,7 @@ import com.android.launcher3.util.TestDispatcherProvider
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -45,15 +46,17 @@ class TaskViewModelTest {
private val recentsViewData = RecentsViewData()
private val getTaskUseCase = mock<GetTaskUseCase>()
private val sut =
TaskViewModel(
recentsViewData = recentsViewData,
getTaskUseCase = getTaskUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
private lateinit var sut: TaskViewModel
@Before
fun setUp() {
sut =
TaskViewModel(
taskViewType = TaskViewType.SINGLE,
recentsViewData = recentsViewData,
getTaskUseCase = getTaskUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
whenever(getTaskUseCase.invoke(TASK_MODEL_2.id)).thenReturn(flow { emit(TASK_MODEL_2) })
whenever(getTaskUseCase.invoke(TASK_MODEL_3.id)).thenReturn(flow { emit(TASK_MODEL_3) })
@@ -65,10 +68,38 @@ class TaskViewModelTest {
fun singleTaskRetrieved_when_validTaskId() =
testScope.runTest {
sut.bind(TASK_MODEL_1.id)
val expectedResult = TaskTileUiState(listOf(TASK_MODEL_1.toUiState()), false)
val expectedResult =
TaskTileUiState(
tasks = listOf(TASK_MODEL_1.toUiState()),
isLiveTile = false,
hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@Test
fun hasHeader_when_taskViewTypeIsDesktop() =
testScope.runTest {
val expectedResults =
mapOf(
TaskViewType.SINGLE to false,
TaskViewType.GROUPED to false,
TaskViewType.DESKTOP to true,
)
expectedResults.forEach { (type, expectedResult) ->
sut =
TaskViewModel(
taskViewType = type,
recentsViewData = recentsViewData,
getTaskUseCase = getTaskUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
sut.bind(TASK_MODEL_1.id)
assertThat(sut.state.first().hasHeader).isEqualTo(expectedResult)
}
}
@Test
fun multipleTasksRetrieved_when_validTaskIds() =
testScope.runTest {
@@ -83,6 +114,7 @@ class TaskViewModelTest {
TaskData.NoData(INVALID_TASK_ID),
),
isLiveTile = false,
hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -103,6 +135,7 @@ class TaskViewModelTest {
TASK_MODEL_3.toUiState(),
),
isLiveTile = true,
hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -123,6 +156,7 @@ class TaskViewModelTest {
TASK_MODEL_3.toUiState(),
),
isLiveTile = false,
hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -142,6 +176,7 @@ class TaskViewModelTest {
TASK_MODEL_3.toUiState(),
),
isLiveTile = false,
hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -157,6 +192,7 @@ class TaskViewModelTest {
TaskTileUiState(
tasks = listOf(TASK_MODEL_1.toUiState(), TASK_MODEL_2.toUiState()),
isLiveTile = false,
hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -166,7 +202,11 @@ class TaskViewModelTest {
testScope.runTest {
sut.bind(INVALID_TASK_ID)
val expectedResult =
TaskTileUiState(listOf(TaskData.NoData(INVALID_TASK_ID)), isLiveTile = false)
TaskTileUiState(
listOf(TaskData.NoData(INVALID_TASK_ID)),
isLiveTile = false,
hasHeader = false,
)
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
@@ -174,6 +214,7 @@ class TaskViewModelTest {
TaskData.Data(
taskId = id,
title = title,
titleDescription = titleDescription,
icon = icon!!,
thumbnailData = thumbnail,
backgroundColor = backgroundColor,
@@ -16,37 +16,16 @@
package com.android.quickstep.task.thumbnail
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.drawable.Drawable
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.Flags
import com.android.launcher3.util.TestDispatcherProvider
import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
import com.android.quickstep.recents.data.FakeTasksRepository
import com.android.quickstep.recents.data.RecentsDeviceProfile
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
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.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
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.StandardTestDispatcher
@@ -69,8 +48,6 @@ class TaskThumbnailViewModelImplTest {
private val recentsViewData = RecentsViewData()
private val taskContainerData = TaskContainerData()
private val dispatcherProvider = TestDispatcherProvider(dispatcher)
private val tasksRepository = FakeTasksRepository()
private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val splashAlphaUseCase: SplashAlphaUseCase = mock()
@@ -79,208 +56,11 @@ class TaskThumbnailViewModelImplTest {
recentsViewData,
taskContainerData,
dispatcherProvider,
tasksRepository,
deviceProfileRepository,
mGetThumbnailPositionUseCase,
splashAlphaUseCase,
)
}
private val fullscreenTaskIdRange: IntRange = 0..5
private val freeformTaskIdRange: IntRange = 6..10
private val fullscreenTasks = fullscreenTaskIdRange.map(::createTaskWithId)
private val freeformTasks = freeformTaskIdRange.map(::createFreeformTaskWithId)
private val tasks = fullscreenTasks + freeformTasks
@Test
fun initialStateIsUninitialized() =
testScope.runTest { assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) }
@Test
fun bindRunningTask_thenStateIs_LiveTile() =
testScope.runTest {
val taskId = 1
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(setOf(taskId))
recentsViewData.runningTaskIds.value = setOf(taskId)
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
}
@Test
fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() =
testScope.runTest {
val taskId = 1
val expectedThumbnailData = createThumbnailData()
tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
val expectedIconData = mock<Drawable>()
tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(setOf(taskId))
recentsViewData.runningTaskIds.value = setOf(taskId)
recentsViewData.runningTaskShowScreenshot.value = true
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
Snapshot.WithoutHeader(
backgroundColor = Color.rgb(1, 1, 1),
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
),
expectedIconData,
)
)
}
@Test
fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
testScope.runTest {
val runningTaskId = 1
val stoppedTaskId = 2
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
recentsViewData.runningTaskIds.value = setOf(runningTaskId)
systemUnderTest.bind(runningTaskId)
assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
systemUnderTest.bind(stoppedTaskId)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
}
@Test
fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() =
testScope.runTest {
val stoppedTaskId = 2
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
systemUnderTest.bind(stoppedTaskId)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
}
@Test
fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() =
testScope.runTest {
val taskId = 2
tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
tasks[taskId].isLocked = true
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(setOf(taskId))
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
}
@Test
fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() =
testScope.runTest {
val taskId = 2
val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
val expectedIconData = mock<Drawable>()
tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(setOf(taskId))
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
Snapshot.WithoutHeader(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_270,
),
expectedIconData,
)
)
}
@Test
fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() =
testScope.runTest {
val taskId = 2
val expectedThumbnailData = createThumbnailData()
tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
val expectedIconData = mock<Drawable>()
tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
tasksRepository.seedTasks(tasks)
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
tasksRepository.setVisibleTasks(setOf(taskId))
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
Snapshot.WithoutHeader(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
),
expectedIconData,
)
)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
fun bindRunningTask_inDesktop_thenStateIs_LiveTile_withHeader() =
testScope.runTest {
deviceProfileRepository.setRecentsDeviceProfile(
RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)
)
val taskId = freeformTaskIdRange.first
val expectedIconData = mock<Drawable>()
tasksRepository.seedIconData(taskId, "Task $taskId", "Task $taskId", expectedIconData)
tasksRepository.seedTasks(freeformTasks)
tasksRepository.setVisibleTasks(setOf(taskId))
recentsViewData.runningTaskIds.value = setOf(taskId)
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(LiveTile.WithHeader(ThumbnailHeader(expectedIconData, "Task $taskId")))
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
fun bindStoppedTaskWithThumbnail_inDesktop_thenStateIs_SnapshotSplash_withHeader() =
testScope.runTest {
deviceProfileRepository.setRecentsDeviceProfile(
RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)
)
val taskId = freeformTaskIdRange.first
val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_0)
tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
val expectedIconData = mock<Drawable>()
tasksRepository.seedIconData(taskId, "Task $taskId", "Task $taskId", expectedIconData)
tasksRepository.seedTasks(freeformTasks)
tasksRepository.setVisibleTasks(setOf(taskId))
systemUnderTest.bind(taskId)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
SnapshotSplash(
Snapshot.WithHeader(
backgroundColor = Color.rgb(taskId, taskId, taskId),
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
header = ThumbnailHeader(expectedIconData, "Task $taskId"),
),
expectedIconData,
)
)
}
@Test
fun getSnapshotMatrix_MissingThumbnail() =
testScope.runTest {
@@ -337,51 +117,7 @@ class TaskThumbnailViewModelImplTest {
assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
}
private fun createTaskWithId(taskId: Int) =
Task(
Task.TaskKey(
taskId,
WINDOWING_MODE_FULLSCREEN,
Intent(),
ComponentName("", ""),
0,
2000,
)
)
.apply {
colorBackground = Color.argb(taskId, taskId, taskId, taskId)
titleDescription = "Task $taskId"
icon = mock<Drawable>()
}
private fun createFreeformTaskWithId(taskId: Int) =
Task(
Task.TaskKey(
taskId,
WINDOWING_MODE_FREEFORM,
Intent(),
ComponentName("", ""),
0,
2000,
)
)
.apply {
colorBackground = Color.argb(taskId, taskId, taskId, taskId)
titleDescription = "Task $taskId"
icon = mock<Drawable>()
}
private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
val bitmap = mock<Bitmap>()
whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
return ThumbnailData(thumbnail = bitmap, rotation = rotation)
}
companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
private companion object {
const val CANVAS_WIDTH = 300
const val CANVAS_HEIGHT = 600
val MATRIX =