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

Commit 1d73fdd4 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add icon loading to TasksRepository" into main

parents d529345e b59d8611
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
@@ -38,7 +39,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 =
@@ -46,10 +47,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
        }
@@ -79,7 +89,6 @@ class TasksRepository(
                    suspendCancellableCoroutine { continuation ->
                        val cancellableTask =
                            taskThumbnailDataSource.getThumbnailInBackground(task) {
                                task.thumbnail = it
                                continuation.resume(it)
                            }
                        continuation.invokeOnCancellation { cancellableTask?.cancel() }
@@ -109,6 +118,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