Merge "Adds View screenshot tests for TaskThumbnailView." into main
This commit is contained in:
@@ -34,6 +34,7 @@ import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
|
||||
import com.android.quickstep.task.viewmodel.TaskContainerData
|
||||
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
|
||||
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
|
||||
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
|
||||
import com.android.quickstep.task.viewmodel.TaskViewData
|
||||
import com.android.quickstep.task.viewmodel.TaskViewModel
|
||||
import com.android.quickstep.views.TaskViewType
|
||||
@@ -180,7 +181,7 @@ class RecentsDependencies private constructor(private val appContext: Context) {
|
||||
TaskContainerData::class.java -> TaskContainerData()
|
||||
TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
|
||||
TaskThumbnailViewModel::class.java ->
|
||||
TaskThumbnailViewModel(
|
||||
TaskThumbnailViewModelImpl(
|
||||
recentsViewData = inject(),
|
||||
taskViewData = inject(scopeId, extras),
|
||||
taskContainerData = inject(scopeId),
|
||||
|
||||
@@ -10,144 +10,40 @@
|
||||
* 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 goveryning permissions and
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.quickstep.task.viewmodel
|
||||
|
||||
import android.annotation.ColorInt
|
||||
import android.app.ActivityTaskManager.INVALID_TASK_ID
|
||||
import android.graphics.Matrix
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.android.quickstep.recents.data.RecentTasksRepository
|
||||
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.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.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TaskThumbnailViewModel(
|
||||
recentsViewData: RecentsViewData,
|
||||
taskViewData: TaskViewData,
|
||||
taskContainerData: TaskContainerData,
|
||||
private val tasksRepository: RecentTasksRepository,
|
||||
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
|
||||
private val splashAlphaUseCase: SplashAlphaUseCase,
|
||||
) {
|
||||
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
|
||||
private val splashProgress = MutableStateFlow(flowOf(0f))
|
||||
private var taskId: Int = INVALID_TASK_ID
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** ViewModel for representing TaskThumbnails */
|
||||
interface TaskThumbnailViewModel {
|
||||
/**
|
||||
* Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
|
||||
* corner radius.
|
||||
*/
|
||||
val cornerRadiusProgress =
|
||||
if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
|
||||
else MutableStateFlow(1f).asStateFlow()
|
||||
val cornerRadiusProgress: StateFlow<Float>
|
||||
|
||||
val inheritedScale =
|
||||
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
|
||||
recentsScale * taskScale
|
||||
}
|
||||
/** The accumulated View.scale value for parent Views up to and including RecentsView */
|
||||
val inheritedScale: Flow<Float>
|
||||
|
||||
val dimProgress: Flow<Float> =
|
||||
combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
|
||||
taskMenuOpenProgress,
|
||||
tintAmount ->
|
||||
max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
|
||||
}
|
||||
val splashAlpha = splashProgress.flatMapLatest { it }
|
||||
/** Provides the level of dimming that the View should have */
|
||||
val dimProgress: Flow<Float>
|
||||
|
||||
private val isLiveTile =
|
||||
combine(
|
||||
task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
|
||||
recentsViewData.runningTaskIds,
|
||||
recentsViewData.runningTaskShowScreenshot,
|
||||
) { taskId, runningTaskIds, runningTaskShowScreenshot ->
|
||||
runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
/** Provides the alpha of the splash icon */
|
||||
val splashAlpha: Flow<Float>
|
||||
|
||||
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 -> LiveTile
|
||||
isBackgroundOnly(taskVal) ->
|
||||
BackgroundOnly(taskVal.colorBackground.removeAlpha())
|
||||
isSnapshotSplashState(taskVal) ->
|
||||
SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
|
||||
else -> Uninitialized
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
/** Provides the UiState by which the task thumbnail can be represented */
|
||||
val uiState: Flow<TaskThumbnailUiState>
|
||||
|
||||
fun bind(taskId: Int) {
|
||||
Log.d(TAG, "bind taskId: $taskId")
|
||||
this.taskId = taskId
|
||||
task.value = tasksRepository.getTaskDataById(taskId)
|
||||
splashProgress.value = splashAlphaUseCase.execute(taskId)
|
||||
}
|
||||
/** Attaches this ViewModel to a specific task id for it to provide data from. */
|
||||
fun bind(taskId: Int)
|
||||
|
||||
fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
|
||||
return runBlocking {
|
||||
when (
|
||||
val thumbnailPositionState =
|
||||
getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
|
||||
) {
|
||||
is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
|
||||
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!!
|
||||
return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
|
||||
}
|
||||
|
||||
@ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
|
||||
|
||||
private companion object {
|
||||
const val MAX_SCRIM_ALPHA = 0.4f
|
||||
const val TAG = "TaskThumbnailViewModel"
|
||||
}
|
||||
/** Returns a Matrix which can be applied to the snapshot */
|
||||
fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language goveryning permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.quickstep.task.viewmodel
|
||||
|
||||
import android.annotation.ColorInt
|
||||
import android.app.ActivityTaskManager.INVALID_TASK_ID
|
||||
import android.graphics.Matrix
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.android.quickstep.recents.data.RecentTasksRepository
|
||||
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.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.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TaskThumbnailViewModelImpl(
|
||||
recentsViewData: RecentsViewData,
|
||||
taskViewData: TaskViewData,
|
||||
taskContainerData: TaskContainerData,
|
||||
private val tasksRepository: RecentTasksRepository,
|
||||
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
|
||||
|
||||
override val cornerRadiusProgress =
|
||||
if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
|
||||
else MutableStateFlow(1f).asStateFlow()
|
||||
|
||||
override val inheritedScale =
|
||||
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
|
||||
recentsScale * taskScale
|
||||
}
|
||||
|
||||
override val dimProgress: Flow<Float> =
|
||||
combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
|
||||
taskMenuOpenProgress,
|
||||
tintAmount ->
|
||||
max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
|
||||
}
|
||||
override val splashAlpha = splashProgress.flatMapLatest { it }
|
||||
|
||||
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 -> LiveTile
|
||||
isBackgroundOnly(taskVal) ->
|
||||
BackgroundOnly(taskVal.colorBackground.removeAlpha())
|
||||
isSnapshotSplashState(taskVal) ->
|
||||
SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
|
||||
else -> Uninitialized
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override fun bind(taskId: Int) {
|
||||
Log.d(TAG, "bind taskId: $taskId")
|
||||
this.taskId = taskId
|
||||
task.value = tasksRepository.getTaskDataById(taskId)
|
||||
splashProgress.value = splashAlphaUseCase.execute(taskId)
|
||||
}
|
||||
|
||||
override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
|
||||
return runBlocking {
|
||||
when (
|
||||
val thumbnailPositionState =
|
||||
getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
|
||||
) {
|
||||
is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
|
||||
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!!
|
||||
return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
|
||||
}
|
||||
|
||||
@ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
|
||||
|
||||
private companion object {
|
||||
const val MAX_SCRIM_ALPHA = 0.4f
|
||||
const val TAG = "TaskThumbnailViewModel"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user