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

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

Merge changes I54ea7a71,Ifd9c54fd into main

* changes:
  Set task properties to prevent the task being null. This behaviour is expected by existing callers and was likely broken by ag/28151977
  TaskRepository performance improvement
parents 80d18e01 36402ada
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -116,6 +116,10 @@ public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListe
                () -> getCacheEntry(task),
                MAIN_EXECUTOR,
                result -> {
                    task.icon = result.icon;
                    task.titleDescription = result.contentDescription;
                    task.title = result.title;

                    callback.onTaskIconReceived(
                            result.icon,
                            result.contentDescription,
+1 −1
Original line number Diff line number Diff line
@@ -40,5 +40,5 @@ interface RecentTasksRepository {
     * Sets the tasks that are visible, indicating that properties relating to visuals need to be
     * populated e.g. icons/thumbnails etc.
     */
    fun setVisibleTasks(visibleTaskIdList: List<Int>)
    fun setVisibleTasks(visibleTaskIdList: Set<Int>)
}
+112 −122
Original line number Diff line number Diff line
@@ -23,147 +23,147 @@ import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconCha
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext

@OptIn(ExperimentalCoroutinesApi::class)
class TasksRepository(
    private val recentsModel: RecentTasksDataSource,
    private val taskThumbnailDataSource: TaskThumbnailDataSource,
    private val taskIconDataSource: TaskIconDataSource,
    private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate,
    recentsCoroutineScope: CoroutineScope,
    private val recentsCoroutineScope: CoroutineScope,
    private val dispatcherProvider: DispatcherProvider,
) : RecentTasksRepository {
    private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
    private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
    private val tasks = MutableStateFlow(MapForStateFlow<Int, Task>(emptyMap()))
    private val taskRequests = HashMap<Int, Pair<Task.TaskKey, Job>>()

    private val taskData =
        groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
    private val visibleTasks =
        combine(taskData, visibleTaskIds) { tasks, visibleIds ->
            tasks.filter { it.key.id in visibleIds }
        }

    private val iconQueryResults: Flow<Map<Int, TaskIconQueryResponse?>> =
        visibleTasks
            .map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
            .flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
                if (iconRequestFlows.isEmpty()) {
                    flowOf(emptyMap())
                } else {
                    combine(iconRequestFlows) { it.toMap() }
    override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
        if (forceRefresh) {
            recentsModel.getTasks { result ->
                tasks.value =
                    MapForStateFlow(
                        result
                            .flatMap { groupTask -> groupTask.tasks }
                            .associateBy { it.key.id }
                            .also {
                                // Clean tasks that are not in the latest group tasks list.
                                val tasksNoLongerVisible = it.keys.subtract(tasks.value.keys)
                                removeTasks(tasksNoLongerVisible)
                            }
                    )
            }
            .distinctUntilChanged()

    private val thumbnailQueryResults: Flow<Map<Int, ThumbnailData?>> =
        visibleTasks
            .map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
            .flatMapLatest { thumbnailRequestFlows: List<ThumbnailDataRequest> ->
                if (thumbnailRequestFlows.isEmpty()) {
                    flowOf(emptyMap())
                } else {
                    combine(thumbnailRequestFlows) { it.toMap() }
        }
        return tasks.map { it.values.toList() }
    }
            .distinctUntilChanged()

    private val augmentedTaskData: Flow<List<Task>> =
        combine(taskData, thumbnailQueryResults, iconQueryResults) {
                tasks,
                thumbnailQueryResults,
                iconQueryResults ->
                tasks.onEach { task ->
                    // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible)
                    task.thumbnail = thumbnailQueryResults[task.key.id]
    override fun getTaskDataById(taskId: Int) = tasks.map { it[taskId] }

                    // TODO(b/352331675) don't load icons for DesktopTaskView
                    // Add retrieved icons + remove unnecessary icons
                    val iconQueryResult = iconQueryResults[task.key.id]
                    task.icon = iconQueryResult?.icon
                    task.titleDescription = iconQueryResult?.contentDescription
                    task.title = iconQueryResult?.title
                }
            }
            .flowOn(dispatcherProvider.io)
            .shareIn(recentsCoroutineScope, SharingStarted.WhileSubscribed(5000), replay = 1)
    override fun getThumbnailById(taskId: Int) =
        getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }

    override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
        if (forceRefresh) {
            recentsModel.getTasks { groupedTaskData.value = it }
        }
        return augmentedTaskData
    override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
        Log.d(TAG, "setVisibleTasks: $visibleTaskIdList")

        // Remove tasks are no longer visible
        val tasksNoLongerVisible = taskRequests.keys.subtract(visibleTaskIdList)
        removeTasks(tasksNoLongerVisible)
        // Add new tasks to be requested
        visibleTaskIdList.subtract(taskRequests.keys).forEach { taskId -> requestTaskData(taskId) }
    }

    private fun requestTaskData(taskId: Int) {
        Log.i(TAG, "requestTaskData: $taskId")
        val task = tasks.value[taskId] ?: return
        taskRequests[taskId] =
            Pair(
                task.key,
                recentsCoroutineScope.launch {
                    fetchIcon(task)
                    fetchThumbnail(task)
                },
            )
    }

    override fun getTaskDataById(taskId: Int): Flow<Task?> =
        augmentedTaskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
    private fun removeTasks(tasksToRemove: Set<Int>) {
        if (tasksToRemove.isEmpty()) return

    override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
        getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
        tasksToRemove.forEach { taskId ->
            Log.i(TAG, "removeTask: $taskId")
            val request = taskRequests.remove(taskId) ?: return
            val (taskKey, job) = request
            job.cancel()

    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
        Log.d(TAG, "setVisibleTasks: $visibleTaskIdList")
        this.visibleTaskIds.value = visibleTaskIdList.toSet()
            // un-registering callbacks
            taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(taskKey)
            taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(taskKey)

            // Clearing Task to reduce memory footprint
            tasks.value[taskId]?.apply {
                thumbnail = null
                icon = null
                title = null
                titleDescription = null
            }
        }
        tasks.update { oldValue -> MapForStateFlow(oldValue) }
    }

    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow {
        trySend(task.key.id to task.thumbnail)
        trySend(task.key.id to getThumbnailFromDataSource(task))
    private suspend fun fetchIcon(task: Task) {
        updateIcon(task.key.id, getIconFromDataSource(task)) // Fetch icon from cache
        taskVisualsChangedDelegate.registerTaskIconChangedCallback(
            task.key,
            object : TaskIconChangedCallback {
                override fun onTaskIconChanged() {
                    recentsCoroutineScope.launch {
                        updateIcon(task.key.id, getIconFromDataSource(task))
                    }
                }
            },
        )
    }

        val callback =
    private suspend fun fetchThumbnail(task: Task) {
        updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(
            task.key,
            object : TaskThumbnailChangedCallback {
                override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) {
                    trySend(task.key.id to thumbnailData)
                    updateThumbnail(task.key.id, thumbnailData)
                }

                override fun onHighResLoadingStateChanged() {
                    launch { trySend(task.key.id to getThumbnailFromDataSource(task)) }
                    recentsCoroutineScope.launch {
                        updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
                    }
                }
        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback)
        awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) }
            },
        )
    }

    /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */
    private fun getIconDataRequest(task: Task): IconDataRequest =
        callbackFlow {
                trySend(task.key.id to task.getTaskIconQueryResponse())
                trySend(task.key.id to getIconFromDataSource(task))

                val callback =
                    object : TaskIconChangedCallback {
                        override fun onTaskIconChanged() {
                            launch { trySend(task.key.id to getIconFromDataSource(task)) }
                        }
                    }
                taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback)
                awaitClose {
                    taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key)
    private fun updateIcon(taskId: Int, iconData: IconData) {
        val task = tasks.value[taskId] ?: return
        task.icon = iconData.icon
        task.titleDescription = iconData.contentDescription
        task.title = iconData.title
        tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
    }

    private fun updateThumbnail(taskId: Int, thumbnail: ThumbnailData?) {
        val task = tasks.value[taskId] ?: return
        task.thumbnail = thumbnail
        tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
    }
            .distinctUntilChanged()

    private suspend fun getThumbnailFromDataSource(task: Task) =
        withContext(dispatcherProvider.main) {
@@ -184,11 +184,7 @@ class TasksRepository(
                        ->
                        icon.constantState?.let {
                            continuation.resume(
                                TaskIconQueryResponse(
                                    it.newDrawable().mutate(),
                                    contentDescription,
                                    title,
                                )
                                IconData(it.newDrawable().mutate(), contentDescription, title)
                            )
                        }
                    }
@@ -199,22 +195,16 @@ class TasksRepository(
    companion object {
        private const val TAG = "TasksRepository"
    }
}

data class TaskIconQueryResponse(
    /** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */
    private data class MapForStateFlow<K, T>(
        private val backingMap: Map<K, T>,
        private val updated: Long = System.nanoTime(),
    ) : Map<K, T> by backingMap

    private data class IconData(
        val icon: Drawable,
        val contentDescription: String,
        val title: String,
    )

private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
    val iconVal = icon ?: return null
    val titleDescriptionVal = titleDescription ?: return null
    val titleVal = title ?: return null

    return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal)
}

private typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>

private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
+1 −1
Original line number Diff line number Diff line
@@ -31,7 +31,7 @@ class RecentsViewModel(
    }

    fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
        recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
        recentsTasksRepository.setVisibleTasks(visibleTaskIdList.toSet())
    }

    fun updateScale(scale: Float) {
+17 −9
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.quickstep.recents.data

import android.graphics.drawable.Drawable
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.flow.Flow
@@ -25,9 +26,9 @@ import kotlinx.coroutines.flow.map

class FakeTasksRepository : RecentTasksRepository {
    private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
    private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
    private var taskIconDataMap: Map<Int, FakeIconData> = emptyMap()
    private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
    private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
    private var visibleTasks: MutableStateFlow<Set<Int>> = MutableStateFlow(emptySet())

    override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks

@@ -48,16 +49,16 @@ class FakeTasksRepository : RecentTasksRepository {
    override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
        getTaskDataById(taskId).map { it?.thumbnail }

    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
    override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
        visibleTasks.value = visibleTaskIdList
        tasks.value =
            tasks.value.map {
                it.apply {
                    thumbnail = thumbnailDataMap[it.key.id]
                    taskIconDataMap[it.key.id].let { taskIconData ->
                        icon = taskIconData?.icon
                        titleDescription = taskIconData?.contentDescription
                        title = taskIconData?.title
                    taskIconDataMap[it.key.id].let { data ->
                        title = data?.title
                        titleDescription = data?.titleDescription
                        icon = data?.icon
                    }
                }
            }
@@ -71,7 +72,14 @@ class FakeTasksRepository : RecentTasksRepository {
        this.thumbnailDataMap = thumbnailDataMap
    }

    fun seedIconData(iconDataMap: Map<Int, TaskIconQueryResponse>) {
        this.taskIconDataMap = iconDataMap
    fun seedIconData(id: Int, title: String, contentDescription: String, icon: Drawable) {
        val iconData = FakeIconData(icon, contentDescription, title)
        this.taskIconDataMap = mapOf(id to iconData)
    }

    private data class FakeIconData(
        val icon: Drawable,
        val titleDescription: String,
        val title: String,
    )
}
Loading