1 /* <lambda>null2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.recents.ui.viewmodel 18 19 import android.annotation.ColorInt 20 import android.util.Log 21 import androidx.core.graphics.ColorUtils 22 import com.android.launcher3.util.coroutines.DispatcherProvider 23 import com.android.quickstep.recents.domain.model.TaskId 24 import com.android.quickstep.recents.domain.model.TaskModel 25 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase 26 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase 27 import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase 28 import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase 29 import com.android.quickstep.recents.domain.usecase.ThumbnailPosition 30 import com.android.quickstep.recents.viewmodel.RecentsViewData 31 import com.android.quickstep.views.TaskViewType 32 import com.android.systemui.shared.recents.model.ThumbnailData 33 import kotlinx.coroutines.ExperimentalCoroutinesApi 34 import kotlinx.coroutines.flow.Flow 35 import kotlinx.coroutines.flow.MutableStateFlow 36 import kotlinx.coroutines.flow.combine 37 import kotlinx.coroutines.flow.distinctUntilChanged 38 import kotlinx.coroutines.flow.flatMapLatest 39 import kotlinx.coroutines.flow.flowOn 40 import kotlinx.coroutines.flow.map 41 42 /** 43 * ViewModel used for [com.android.quickstep.views.TaskView], 44 * [com.android.quickstep.views.DesktopTaskView] and [com.android.quickstep.views.GroupedTaskView]. 45 */ 46 @OptIn(ExperimentalCoroutinesApi::class) 47 class TaskViewModel( 48 private val taskViewType: TaskViewType, 49 recentsViewData: RecentsViewData, 50 private val getTaskUseCase: GetTaskUseCase, 51 private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase, 52 private val isThumbnailValidUseCase: IsThumbnailValidUseCase, 53 private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase, 54 dispatcherProvider: DispatcherProvider, 55 ) { 56 private var taskIds = MutableStateFlow(emptySet<Int>()) 57 58 private val isLiveTile = 59 combine( 60 taskIds, 61 recentsViewData.runningTaskIds, 62 recentsViewData.runningTaskShowScreenshot, 63 ) { taskIds, runningTaskIds, runningTaskShowScreenshot -> 64 runningTaskIds == taskIds && !runningTaskShowScreenshot 65 } 66 .distinctUntilChanged() 67 68 private val isCentralTask = 69 combine(taskIds, recentsViewData.centralTaskIds) { taskIds, centralTaskIds -> 70 taskIds == centralTaskIds 71 } 72 .distinctUntilChanged() 73 74 private val taskData = 75 taskIds.flatMapLatest { ids -> 76 // Combine Tasks requests 77 combine( 78 ids.map { id -> getTaskUseCase(id).map { taskModel -> id to taskModel } }, 79 ::mapToTaskData, 80 ) 81 } 82 83 private val overlayEnabled = 84 combine(recentsViewData.overlayEnabled, recentsViewData.settledFullyVisibleTaskIds) { 85 isOverlayEnabled, 86 settledFullyVisibleTaskIds -> 87 isOverlayEnabled && settledFullyVisibleTaskIds.any { it in taskIds.value } 88 } 89 .distinctUntilChanged() 90 91 val state: Flow<TaskTileUiState> = 92 combine(taskData, isLiveTile, overlayEnabled, isCentralTask, ::mapToTaskTile) 93 .distinctUntilChanged() 94 .flowOn(dispatcherProvider.background) 95 96 fun bind(vararg taskId: TaskId) { 97 taskIds.value = taskId.toSet().also { Log.d(TAG, "bind: $it") } 98 } 99 100 fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean = 101 isThumbnailValidUseCase(thumbnail, width, height) 102 103 fun getThumbnailPosition( 104 thumbnail: ThumbnailData?, 105 width: Int, 106 height: Int, 107 isRtl: Boolean, 108 ): ThumbnailPosition = 109 getThumbnailPositionUseCase( 110 thumbnailData = thumbnail, 111 width = width, 112 height = height, 113 isRtl = isRtl, 114 ) 115 116 private fun mapToTaskTile( 117 tasks: List<TaskData>, 118 isLiveTile: Boolean, 119 overlayEnabled: Boolean, 120 isCentralTask: Boolean, 121 ): TaskTileUiState { 122 val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData 123 return TaskTileUiState( 124 tasks = tasks, 125 isLiveTile = isLiveTile, 126 hasHeader = taskViewType == TaskViewType.DESKTOP, 127 sysUiStatusNavFlags = getSysUiStatusNavFlagsUseCase(firstThumbnailData), 128 taskOverlayEnabled = overlayEnabled, 129 isCentralTask = isCentralTask, 130 ) 131 } 132 133 private fun mapToTaskData(result: Array<Pair<TaskId, TaskModel?>>): List<TaskData> = 134 result.map { mapToTaskData(it.first, it.second) } 135 136 private fun mapToTaskData(taskId: TaskId, result: TaskModel?): TaskData = 137 result?.let { 138 TaskData.Data( 139 taskId = taskId, 140 title = result.title, 141 titleDescription = result.titleDescription, 142 icon = result.icon, 143 thumbnailData = result.thumbnail, 144 backgroundColor = result.backgroundColor.removeAlpha(), 145 isLocked = result.isLocked, 146 ) 147 } ?: TaskData.NoData(taskId) 148 149 @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff) 150 151 private companion object { 152 const val TAG = "TaskViewModel" 153 } 154 } 155