Loading quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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), Loading quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt +17 −121 Original line number Diff line number Diff line Loading @@ -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 } quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt 0 → 100644 +149 −0 Original line number Diff line number Diff line /* * 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" } } quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt 0 → 100644 +37 −0 Original line number Diff line number Diff line /* * 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 } quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line /* * 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, ) } } Loading
quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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), Loading
quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt +17 −121 Original line number Diff line number Diff line Loading @@ -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 }
quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt 0 → 100644 +149 −0 Original line number Diff line number Diff line /* * 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" } }
quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt 0 → 100644 +37 −0 Original line number Diff line number Diff line /* * 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 }
quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line /* * 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, ) } }