Merge "Refactor: Remove TaskThumbnailViewModel and introduce TaskViewModel" into main
This commit is contained in:
@@ -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()) {
|
||||
|
||||
-2
@@ -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
|
||||
|
||||
+9
-18
@@ -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)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+231
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+49
-8
@@ -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,
|
||||
|
||||
+1
-265
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user