Merge "Adds View screenshot tests for TaskThumbnailView." into main

This commit is contained in:
Uwais Ashraf
2024-10-10 11:38:19 +00:00
committed by Android (Google) Code Review
8 changed files with 311 additions and 134 deletions
@@ -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"
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.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 cornerRadiusProgress = MutableStateFlow(0f)
override val inheritedScale = MutableStateFlow(1f)
override val dimProgress = MutableStateFlow(0f)
override val splashAlpha = MutableStateFlow(0f)
override val uiState = MutableStateFlow<TaskThumbnailUiState>(Uninitialized)
override fun bind(taskId: Int) {
// no-op
}
override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean) =
Matrix.IDENTITY_MATRIX
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.task.thumbnail
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import com.android.launcher3.R
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
import platform.test.screenshot.DeviceEmulationSpec
import platform.test.screenshot.Displays
import platform.test.screenshot.ViewScreenshotTestRule
import platform.test.screenshot.getEmulatedDevicePathConfig
/** Screenshot tests for [TaskThumbnailView]. */
@RunWith(ParameterizedAndroidJunit4::class)
class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
@get:Rule
val screenshotRule =
ViewScreenshotTestRule(
emulationSpec,
ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
)
private val taskThumbnailViewModel = FakeTaskThumbnailViewModel()
@Test
fun taskThumbnailView_uninitialized() {
screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity)
}
}
@Test
fun taskThumbnailView_backgroundOnly() {
screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity ->
activity.actionBar?.hide()
taskThumbnailViewModel.uiState.value = TaskThumbnailUiState.BackgroundOnly(Color.YELLOW)
createTaskThumbnailView(activity)
}
}
private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
val di = RecentsDependencies.initialize(context)
val taskThumbnailView =
LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
return taskThumbnailView as TaskThumbnailView
}
companion object {
@Parameters(name = "{0}")
@JvmStatic
fun getTestSpecs() =
DeviceEmulationSpec.forDisplays(
Displays.Phone,
isDarkTheme = false,
isLandscape = false,
)
}
}
@@ -22,7 +22,6 @@ import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.util.TestDispatcherProvider
import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
@@ -286,9 +285,14 @@ class TasksRepositoryTest {
private fun createThumbnailData(): ThumbnailData {
val bitmap = mock<Bitmap>()
whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH)
whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT)
whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
return ThumbnailData(thumbnail = bitmap)
}
companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
}
}
@@ -22,7 +22,6 @@ import android.graphics.Bitmap
import android.graphics.Color
import android.view.Surface
import com.android.quickstep.recents.data.FakeTasksRepository
import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
@@ -126,9 +125,14 @@ class RecentsViewModelTest {
private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
val bitmap = mock<Bitmap>()
whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH)
whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT)
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
}
}
@@ -36,7 +36,7 @@ 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.quickstep.task.viewmodel.TaskContainerData
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.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
@@ -51,7 +51,7 @@ import org.mockito.kotlin.whenever
/** Test for [TaskThumbnailView] */
@RunWith(AndroidJUnit4::class)
class TaskThumbnailViewModelTest {
class TaskThumbnailViewModelImplTest {
private var taskViewType = TaskViewType.SINGLE
private val recentsViewData = RecentsViewData()
private val taskViewData by lazy { TaskViewData(taskViewType) }
@@ -60,7 +60,7 @@ class TaskThumbnailViewModelTest {
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val splashAlphaUseCase: SplashAlphaUseCase = mock()
private val systemUnderTest by lazy {
TaskThumbnailViewModel(
TaskThumbnailViewModelImpl(
recentsViewData,
taskViewData,
taskContainerData,
@@ -109,7 +109,7 @@ class TaskThumbnailViewModelTest {
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
),
expectedIconData.icon
expectedIconData.icon,
)
)
}
@@ -204,7 +204,7 @@ class TaskThumbnailViewModelTest {
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_270,
),
expectedIconData.icon
expectedIconData.icon,
)
)
}
@@ -230,7 +230,7 @@ class TaskThumbnailViewModelTest {
bitmap = expectedThumbnailData.thumbnail!!,
thumbnailRotation = Surface.ROTATION_0,
),
expectedIconData.icon
expectedIconData.icon,
)
)
}