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

Commit 71fb897f authored by Uwais Ashraf's avatar Uwais Ashraf Committed by Android (Google) Code Review
Browse files

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

parents f5676523 67d0d59c
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