Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 67d0d59c authored by Uwais Ashraf's avatar Uwais Ashraf
Browse files

Adds View screenshot tests for TaskThumbnailView.

Bug: 344800402
Test: TaskThumbnailViewScreenshotTest
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Change-Id: If6530ea9dfe026589f05731d5362dc10f719fc70
parent 9c9f9a88
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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),
+17 −121
Original line number Diff line number Diff line
@@ -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
}
+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"
    }
}
+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
}
+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