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

Commit b59d8611 authored by Uwais Ashraf's avatar Uwais Ashraf
Browse files

Add icon loading to TasksRepository

Bug: 334826842
Test: TasksRepositoryTest
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Change-Id: I75f2e0e9aae4663993ca54742f653f4c7c04fdfe
parent a00c81c1
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import android.os.UserHandle;
import android.text.TextUtils;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

import com.android.launcher3.R;
@@ -48,6 +49,7 @@ import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.Preconditions;
import com.android.quickstep.task.thumbnail.data.TaskIconDataSource;
import com.android.quickstep.util.TaskKeyLruCache;
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.systemui.shared.recents.model.Task;
@@ -59,7 +61,7 @@ import java.util.concurrent.Executor;
/**
 * Manages the caching of task icons and related data.
 */
public class TaskIconCache implements DisplayInfoChangeListener {
public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListener {

    private final Executor mBgExecutor;

@@ -102,7 +104,8 @@ public class TaskIconCache implements DisplayInfoChangeListener {
     * @param callback The callback to receive the task after its data has been populated.
     * @return A cancelable handle to the request
     */
    public CancellableTask getIconInBackground(Task task, GetTaskIconCallback callback) {
    @Override
    public CancellableTask getIconInBackground(Task task, @NonNull GetTaskIconCallback callback) {
        Preconditions.assertUIThread();
        if (task.icon != null) {
            // Nothing to load, the icon is already loaded
+68 −6
Original line number Diff line number Diff line
@@ -16,7 +16,8 @@

package com.android.quickstep.recents.data

import com.android.quickstep.TaskIconCache
import android.graphics.drawable.Drawable
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
@@ -37,7 +38,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
class TasksRepository(
    private val recentsModel: RecentTasksDataSource,
    private val taskThumbnailDataSource: TaskThumbnailDataSource,
    private val taskIconCache: TaskIconCache,
    private val taskIconDataSource: TaskIconDataSource,
) : RecentTasksRepository {
    private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
    private val _taskData =
@@ -45,10 +46,19 @@ class TasksRepository(
    private val visibleTaskIds = MutableStateFlow(emptySet<Int>())

    private val taskData: Flow<List<Task>> =
        combine(_taskData, getThumbnailQueryResults()) { tasks, results ->
        combine(_taskData, getThumbnailQueryResults(), getIconQueryResults()) {
            tasks,
            thumbnailQueryResults,
            iconQueryResults ->
            tasks.forEach { task ->
                // Add retrieved thumbnails + remove unnecessary thumbnails
                task.thumbnail = results[task.key.id]
                task.thumbnail = thumbnailQueryResults[task.key.id]

                // TODO(b/352331675) don't load icons for DesktopTaskView
                // Add retrieved icons + remove unnecessary icons
                task.icon = iconQueryResults[task.key.id]?.icon
                task.titleDescription = iconQueryResults[task.key.id]?.contentDescription
                task.title = iconQueryResults[task.key.id]?.title
            }
            tasks
        }
@@ -75,7 +85,6 @@ class TasksRepository(
                    suspendCancellableCoroutine { continuation ->
                        val cancellableTask =
                            taskThumbnailDataSource.getThumbnailInBackground(task) {
                                task.thumbnail = it
                                continuation.resume(it)
                            }
                        continuation.invokeOnCancellation { cancellableTask?.cancel() }
@@ -105,6 +114,59 @@ class TasksRepository(
            }
        }
    }

    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
    private fun getIconDataRequest(task: Task): IconDataRequest =
        flow {
                emit(task.key.id to task.getTaskIconQueryResponse())
                val iconDataResponse: TaskIconQueryResponse? =
                    suspendCancellableCoroutine { continuation ->
                        val cancellableTask =
                            taskIconDataSource.getIconInBackground(task) {
                                icon,
                                contentDescription,
                                title ->
                                continuation.resume(
                                    TaskIconQueryResponse(icon, contentDescription, title)
                                )
                            }
                        continuation.invokeOnCancellation { cancellableTask?.cancel() }
                    }
                emit(task.key.id to iconDataResponse)
            }
            .distinctUntilChanged()

    private fun getIconQueryResults(): Flow<Map<Int, TaskIconQueryResponse?>> {
        val visibleTasks =
            combine(_taskData, visibleTaskIds) { tasks, visibleIds ->
                tasks.filter { it.key.id in visibleIds }
            }
        val visibleIconDataRequests: Flow<List<IconDataRequest>> =
            visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
        return visibleIconDataRequests.flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
            if (iconRequestFlows.isEmpty()) {
                flowOf(emptyMap())
            } else {
                combine(iconRequestFlows) { it.toMap() }
            }
        }
    }
}

private data class TaskIconQueryResponse(
    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?>>

typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
+25 −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.data

import com.android.launcher3.util.CancellableTask
import com.android.quickstep.TaskIconCache.GetTaskIconCallback
import com.android.systemui.shared.recents.model.Task

interface TaskIconDataSource {
    fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>?
}
+58 −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.recents.data

import android.graphics.drawable.Drawable
import com.android.launcher3.util.CancellableTask
import com.android.quickstep.TaskIconCache
import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
import com.android.systemui.shared.recents.model.Task
import com.google.common.truth.Truth.assertThat
import org.mockito.kotlin.mock

class FakeTaskIconDataSource : TaskIconDataSource {

    val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mock() }
    val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
    var shouldLoadSynchronously: Boolean = true

    /** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */
    override fun getIconInBackground(
        task: Task,
        callback: TaskIconCache.GetTaskIconCallback
    ): CancellableTask<*>? {
        val wrappedCallback = {
            callback.onTaskIconReceived(
                taskIdToDrawable.getValue(task.key.id),
                "content desc ${task.key.id}",
                "title ${task.key.id}"
            )
        }
        if (shouldLoadSynchronously) {
            wrappedCallback()
        } else {
            taskIdToUpdatingTask[task.key.id] = wrappedCallback
        }
        return null
    }
}

fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) {
    assertThat(icon).isEqualTo(fakeTaskIconDataSource.taskIdToDrawable[key.id])
    assertThat(titleDescription).isEqualTo("content desc ${key.id}")
    assertThat(title).isEqualTo("title ${key.id}")
}
+49 −8
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.quickstep.recents.data

import android.content.ComponentName
import android.content.Intent
import com.android.quickstep.TaskIconCache
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
@@ -31,7 +30,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.mockito.kotlin.mock

@OptIn(ExperimentalCoroutinesApi::class)
class TasksRepositoryTest {
@@ -44,10 +42,10 @@ class TasksRepositoryTest {
        )
    private val recentsModel = FakeRecentTasksDataSource()
    private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
    private val taskIconCache = mock<TaskIconCache>()
    private val taskIconDataSource = FakeTaskIconDataSource()

    private val systemUnderTest =
        TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache)
        TasksRepository(recentsModel, taskThumbnailDataSource, taskIconDataSource)

    @Test
    fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest {
@@ -80,6 +78,22 @@ class TasksRepositoryTest {
            .isEqualTo(bitmap2)
    }

    @Test
    fun setVisibleTasksPopulatesIcons() = runTest {
        recentsModel.seedTasks(defaultTaskList)
        systemUnderTest.getAllTaskData(forceRefresh = true)

        systemUnderTest.setVisibleTasks(listOf(1, 2))

        // .drop(1) to ignore initial null content before from thumbnail was loaded.
        systemUnderTest
            .getTaskDataById(1)
            .drop(1)
            .first()!!
            .assertHasIconDataFromSource(taskIconDataSource)
        systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
    }

    @Test
    fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest {
        recentsModel.seedTasks(defaultTaskList)
@@ -101,7 +115,28 @@ class TasksRepositoryTest {
    }

    @Test
    fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest {
    fun changingVisibleTasksContainsAlreadyPopulatedIcons() = runTest {
        recentsModel.seedTasks(defaultTaskList)
        systemUnderTest.getAllTaskData(forceRefresh = true)

        systemUnderTest.setVisibleTasks(listOf(1, 2))

        // .drop(1) to ignore initial null content before from icon was loaded.
        systemUnderTest
            .getTaskDataById(2)
            .drop(1)
            .first()!!
            .assertHasIconDataFromSource(taskIconDataSource)

        // Prevent new loading of Drawables
        taskThumbnailDataSource.shouldLoadSynchronously = false
        systemUnderTest.setVisibleTasks(listOf(2, 3))

        systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
    }

    @Test
    fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() = runTest {
        recentsModel.seedTasks(defaultTaskList)
        val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
        systemUnderTest.getAllTaskData(forceRefresh = true)
@@ -109,14 +144,20 @@ class TasksRepositoryTest {
        systemUnderTest.setVisibleTasks(listOf(1, 2))

        // .drop(1) to ignore initial null content before from thumbnail was loaded.
        assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail)
            .isEqualTo(bitmap2)
        val task2 = systemUnderTest.getTaskDataById(2).drop(1).first()!!
        assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
        task2.assertHasIconDataFromSource(taskIconDataSource)

        // Prevent new loading of Bitmaps
        taskThumbnailDataSource.shouldLoadSynchronously = false
        taskIconDataSource.shouldLoadSynchronously = false
        systemUnderTest.setVisibleTasks(listOf(0, 1))

        assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
        val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
        assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
        assertThat(task2AfterVisibleTasksChanged.icon).isNull()
        assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull()
        assertThat(task2AfterVisibleTasksChanged.title).isNull()
    }

    @Test