From f6f34ca4412048fb74e38e12ef25454bb72b542c Mon Sep 17 00:00:00 2001 From: Uwais Ashraf Date: Mon, 20 May 2024 21:19:22 +0000 Subject: [PATCH] Add TasksRepository Bug: 334825222 Test: TasksRepositoryTest Flag: com.android.launcher3.enable_refactor_task_thumbnail Change-Id: I3e08dea7b205df54f8bef456ead6466aa2ce45c6 --- .../KeyboardQuickSwitchController.java | 6 +- .../android/quickstep/RecentTasksList.java | 4 +- .../com/android/quickstep/RecentsModel.java | 11 +- .../android/quickstep/TaskThumbnailCache.java | 7 +- .../fallback/FallbackRecentsView.java | 3 +- .../recents/data/RecentTasksDataSource.kt | 24 +++ .../quickstep/recents/data/TasksRepository.kt | 114 +++++++++++++ .../thumbnail/data/TaskThumbnailDataSource.kt | 29 ++++ .../android/quickstep/util/DesktopTask.java | 12 +- .../com/android/quickstep/util/GroupTask.java | 15 ++ .../android/quickstep/views/RecentsView.java | 2 +- .../recents/data/FakeRecentTasksDataSource.kt | 33 ++++ .../data/FakeTaskThumbnailDataSource.kt | 52 ++++++ .../recents/data/TasksRepositoryTest.kt | 150 ++++++++++++++++++ .../util/SplitSelectStateControllerTest.kt | 19 ++- 15 files changed, 456 insertions(+), 25 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt create mode 100644 quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt create mode 100644 quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java index b213203cb1..358d703b75 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -148,7 +148,7 @@ public final class KeyboardQuickSwitchController implements }); } - private void processLoadedTasks(ArrayList tasks) { + private void processLoadedTasks(List tasks) { // Only store MAX_TASK tasks, from most to least recent Collections.reverse(tasks); mTasks = tasks.stream() @@ -157,7 +157,7 @@ public final class KeyboardQuickSwitchController implements mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS); } - private void processLoadedTasksOnDesktop(ArrayList tasks) { + private void processLoadedTasksOnDesktop(List tasks) { // Find the single desktop task that contains a grouping of desktop tasks DesktopTask desktopTask = findDesktopTask(tasks); @@ -173,7 +173,7 @@ public final class KeyboardQuickSwitchController implements } @Nullable - private DesktopTask findDesktopTask(ArrayList tasks) { + private DesktopTask findDesktopTask(List tasks) { return (DesktopTask) tasks.stream() .filter(t -> t instanceof DesktopTask) .findFirst() diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java index 711882c7b6..37b4dcabb8 100644 --- a/quickstep/src/com/android/quickstep/RecentTasksList.java +++ b/quickstep/src/com/android/quickstep/RecentTasksList.java @@ -31,6 +31,7 @@ import android.os.Process; import android.os.RemoteException; import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.util.LooperExecutor; @@ -44,6 +45,7 @@ import com.android.wm.shell.util.GroupedRecentTaskInfo; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -137,7 +139,7 @@ public class RecentTasksList { * @return The change id of the current task list */ public synchronized int getTasks(boolean loadKeysOnly, - Consumer> callback, Predicate filter) { + @Nullable Consumer> callback, Predicate filter) { final int requestLoadId = mChangeId; if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) { // The list is up to date, send the callback on the next frame, diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index 89351aa2d2..98c1eb409c 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -33,6 +33,7 @@ import android.os.Build; import android.os.Process; import android.os.UserHandle; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.IconProvider; @@ -40,6 +41,7 @@ import com.android.launcher3.icons.IconProvider.IconChangeListener; import com.android.launcher3.util.Executors.SimpleThreadFactory; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SafeCloseable; +import com.android.quickstep.recents.data.RecentTasksDataSource; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.systemui.shared.recents.model.Task; @@ -60,8 +62,8 @@ import java.util.function.Predicate; * Singleton class to load and manage recents model. */ @TargetApi(Build.VERSION_CODES.O) -public class RecentsModel implements IconChangeListener, TaskStackChangeListener, - TaskVisualsChangeListener, SafeCloseable { +public class RecentsModel implements RecentTasksDataSource, IconChangeListener, + TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable { // We do not need any synchronization for this variable as its only written on UI thread. public static final MainThreadInitializedObject INSTANCE = @@ -141,7 +143,8 @@ public class RecentsModel implements IconChangeListener, TaskStackChangeListener * always called on the UI thread. * @return the request id associated with this call. */ - public int getTasks(Consumer> callback) { + @Override + public int getTasks(@Nullable Consumer> callback) { return mTaskList.getTasks(false /* loadKeysOnly */, callback, RecentsFilterState.DEFAULT_FILTER); } @@ -155,7 +158,7 @@ public class RecentsModel implements IconChangeListener, TaskStackChangeListener * callback. * @return the request id associated with this call. */ - public int getTasks(Consumer> callback, Predicate filter) { + public int getTasks(@Nullable Consumer> callback, Predicate filter) { return mTaskList.getTasks(false /* loadKeysOnly */, callback, filter); } diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java index 7ebb767f01..38e927f95f 100644 --- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java +++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java @@ -21,11 +21,13 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.content.Context; import android.content.res.Resources; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; import com.android.launcher3.util.CancellableTask; import com.android.launcher3.util.Preconditions; +import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource; import com.android.quickstep.util.TaskKeyByLastActiveTimeCache; import com.android.quickstep.util.TaskKeyCache; import com.android.quickstep.util.TaskKeyLruCache; @@ -38,7 +40,7 @@ import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.function.Consumer; -public class TaskThumbnailCache { +public class TaskThumbnailCache implements TaskThumbnailDataSource { private final Executor mBgExecutor; private final TaskKeyCache mCache; @@ -148,8 +150,9 @@ public class TaskThumbnailCache { * @param callback The callback to receive the task after its data has been populated. * @return A cancelable handle to the request */ + @Override public CancellableTask updateThumbnailInBackground( - Task task, Consumer callback) { + Task task, @NonNull Consumer callback) { Preconditions.assertUIThread(); boolean lowResolution = !mHighResLoadingState.isEnabled(); diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java index 096ed2c0cb..485d6c4391 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java @@ -54,6 +54,7 @@ import com.android.systemui.shared.recents.model.Task; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; public class FallbackRecentsView extends RecentsView implements StateListener { @@ -179,7 +180,7 @@ public class FallbackRecentsView extends RecentsView taskGroups) { + protected void applyLoadPlan(List taskGroups) { // When quick-switching on 3p-launcher, we add a "stub" tile corresponding to Launcher // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to // track the index of the next task appropriately, as if we are switching on any other app. diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt new file mode 100644 index 0000000000..6719099f7b --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksDataSource.kt @@ -0,0 +1,24 @@ +/* + * 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 com.android.quickstep.util.GroupTask +import java.util.function.Consumer + +interface RecentTasksDataSource { + fun getTasks(callback: Consumer>?): Int +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt new file mode 100644 index 0000000000..ad8ae207d1 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt @@ -0,0 +1,114 @@ +/* + * 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 com.android.quickstep.TaskIconCache +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.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.suspendCancellableCoroutine + +@OptIn(ExperimentalCoroutinesApi::class) +class TasksRepository( + private val recentsModel: RecentTasksDataSource, + private val taskThumbnailDataSource: TaskThumbnailDataSource, + private val taskIconCache: TaskIconCache, +) { + private val groupedTaskData = MutableStateFlow(emptyList()) + private val _taskData = + groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } } + private val visibleTaskIds = MutableStateFlow(emptySet()) + + private val taskData: Flow> = + combine(_taskData, getThumbnailQueryResults()) { tasks, results -> + tasks.forEach { task -> + // Add retrieved thumbnails + remove unnecessary thumbnails + task.thumbnail = results[task.key.id] + } + tasks + } + + fun getAllTaskData(forceRefresh: Boolean = false): Flow> { + if (forceRefresh) { + recentsModel.getTasks { groupedTaskData.value = it } + } + return taskData + } + + fun getTaskDataById(taskId: Int): Flow = + taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } } + + fun setVisibleTasks(visibleTaskIdList: List) { + this.visibleTaskIds.value = visibleTaskIdList.toSet() + } + + /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */ + private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = + flow { + emit(task.key.id to task.thumbnail) + val thumbnailDataResult: ThumbnailData? = + suspendCancellableCoroutine { continuation -> + val cancellableTask = + taskThumbnailDataSource.updateThumbnailInBackground(task) { + continuation.resume(it) + } + continuation.invokeOnCancellation { cancellableTask?.cancel() } + } + emit(task.key.id to thumbnailDataResult) + } + .distinctUntilChanged() + + /** + * This is a Flow that makes a query for thumbnail data to the [taskThumbnailDataSource] for + * each visible task. It then collects the responses and returns them in a Map as soon as they + * are available. + */ + private fun getThumbnailQueryResults(): Flow> { + val visibleTasks = + combine(_taskData, visibleTaskIds) { tasks, visibleIds -> + tasks.filter { it.key.id in visibleIds } + } + val visibleThumbnailDataRequests: Flow> = + visibleTasks.map { + it.map { visibleTask -> + val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail } + getThumbnailDataRequest(taskCopy) + } + } + return visibleThumbnailDataRequests.flatMapLatest { + thumbnailRequestFlows: List -> + if (thumbnailRequestFlows.isEmpty()) { + flowOf(emptyMap()) + } else { + combine(thumbnailRequestFlows) { it.toMap() } + } + } + } +} + +typealias ThumbnailDataRequest = Flow> diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt new file mode 100644 index 0000000000..55598f0a2d --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt @@ -0,0 +1,29 @@ +/* + * 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.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import java.util.function.Consumer + +interface TaskThumbnailDataSource { + fun updateThumbnailInBackground( + task: Task, + callback: Consumer + ): CancellableTask? +} diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java index 07f2d68869..8d99069c19 100644 --- a/quickstep/src/com/android/quickstep/util/DesktopTask.java +++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java @@ -21,7 +21,7 @@ import androidx.annotation.NonNull; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; -import java.util.ArrayList; +import java.util.List; /** * A {@link Task} container that can contain N number of tasks that are part of the desktop in @@ -30,9 +30,9 @@ import java.util.ArrayList; public class DesktopTask extends GroupTask { @NonNull - public final ArrayList tasks; + public final List tasks; - public DesktopTask(@NonNull ArrayList tasks) { + public DesktopTask(@NonNull List tasks) { super(tasks.get(0), null, null, TaskView.Type.DESKTOP); this.tasks = tasks; } @@ -52,6 +52,12 @@ public class DesktopTask extends GroupTask { return true; } + @Override + @NonNull + public List getTasks() { + return tasks; + } + @Override public DesktopTask copy() { return new DesktopTask(tasks); diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java index 7dd6afc55c..945ffe31f6 100644 --- a/quickstep/src/com/android/quickstep/util/GroupTask.java +++ b/quickstep/src/com/android/quickstep/util/GroupTask.java @@ -23,6 +23,10 @@ import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + /** * A {@link Task} container that can contain one or two tasks, depending on if the two tasks * are represented as an app-pair in the recents task list. @@ -61,6 +65,17 @@ public class GroupTask { return task2 != null; } + /** + * Returns a List of all the Tasks in this GroupTask + */ + public List getTasks() { + if (task2 == null) { + return Collections.singletonList(task1); + } else { + return Arrays.asList(task1, task2); + } + } + /** * Create a copy of this instance */ diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 4e5d646e27..8ee7dbc154 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -1715,7 +1715,7 @@ public abstract class RecentsView taskGroups) { + protected void applyLoadPlan(List taskGroups) { if (mPendingAnimation != null) { mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups)); return; diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt new file mode 100644 index 0000000000..eaeb513ea5 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt @@ -0,0 +1,33 @@ +/* + * 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 com.android.quickstep.util.GroupTask +import java.util.function.Consumer + +class FakeRecentTasksDataSource : RecentTasksDataSource { + var taskList: List = listOf() + + override fun getTasks(callback: Consumer>?): Int { + callback?.accept(taskList) + return 0 + } + + fun seedTasks(tasks: List) { + taskList = tasks + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt new file mode 100644 index 0000000000..b66b7351bf --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt @@ -0,0 +1,52 @@ +/* + * 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.Bitmap +import com.android.launcher3.util.CancellableTask +import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import java.util.function.Consumer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class FakeTaskThumbnailDataSource : TaskThumbnailDataSource { + + val taskIdToBitmap: Map = (0..10).associateWith { mock() } + val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() + var shouldLoadSynchronously: Boolean = true + + /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */ + override fun updateThumbnailInBackground( + task: Task, + callback: Consumer + ): CancellableTask? { + val thumbnailData = mock() + whenever(thumbnailData.thumbnail).thenReturn(taskIdToBitmap[task.key.id]) + val wrappedCallback = { + task.thumbnail = thumbnailData + callback.accept(thumbnailData) + } + if (shouldLoadSynchronously) { + wrappedCallback() + } else { + taskIdToUpdatingTask[task.key.id] = wrappedCallback + } + return null + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt new file mode 100644 index 0000000000..c28a85a8f8 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt @@ -0,0 +1,150 @@ +/* + * 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.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 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList +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 { + private val tasks = (0..5).map(::createTaskWithId) + private val defaultTaskList = + listOf( + GroupTask(tasks[0]), + GroupTask(tasks[1], tasks[2], null), + DesktopTask(tasks.subList(3, 6)) + ) + private val recentsModel = FakeRecentTasksDataSource() + private val taskThumbnailDataSource = FakeTaskThumbnailDataSource() + private val taskIconCache = mock() + + private val systemUnderTest = + TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache) + + @Test + fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest { + recentsModel.seedTasks(defaultTaskList) + + assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks) + } + + @Test + fun getTaskDataByIdReturnsSpecificTask() = runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2]) + } + + @Test + fun setVisibleTasksPopulatesThumbnails() = runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + // .drop(1) to ignore initial null content before from thumbnail was loaded. + assertThat(systemUnderTest.getTaskDataById(1).drop(1).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap1) + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + 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) + + // Prevent new loading of Bitmaps + taskThumbnailDataSource.shouldLoadSynchronously = false + systemUnderTest.setVisibleTasks(listOf(2, 3)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(forceRefresh = true) + + 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) + + // Prevent new loading of Bitmaps + taskThumbnailDataSource.shouldLoadSynchronously = false + systemUnderTest.setVisibleTasks(listOf(0, 1)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull() + } + + @Test + fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = runTest { + // Setup fakes + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + taskThumbnailDataSource.shouldLoadSynchronously = false + + // Setup TasksRepository + systemUnderTest.getAllTaskData(forceRefresh = true) + systemUnderTest.setVisibleTasks(listOf(1, 2)) + + // Assert there is no bitmap in first emission + val taskFlow = systemUnderTest.getTaskDataById(2) + val taskFlowValuesList = mutableListOf() + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + taskFlow.toList(taskFlowValuesList) + } + assertThat(taskFlowValuesList[0]!!.thumbnail).isNull() + + // Simulate bitmap loading after first emission + taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke() + + // Check for second emission + assertThat(taskFlowValuesList[1]!!.thumbnail!!.thumbnail).isEqualTo(bitmap2) + } + + private fun createTaskWithId(taskId: Int) = + Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)) +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt index 0de5f197ea..aa08ca4c05 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -31,7 +31,6 @@ import com.android.launcher3.logging.StatsLogManager.StatsLogger import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager -import com.android.launcher3.statemanager.StatefulActivity import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.RecentsModel @@ -121,7 +120,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent), false /* findExactPairMatch */, @@ -174,7 +173,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, @@ -215,7 +214,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, @@ -271,7 +270,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, @@ -324,7 +323,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, @@ -378,7 +377,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent, matchingComponent), false /* findExactPairMatch */, @@ -431,7 +430,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, @@ -497,7 +496,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, @@ -549,7 +548,7 @@ class SplitSelectStateControllerTest { // Capture callback from recentsModel#getTasks() val consumer = - argumentCaptor>> { + argumentCaptor>> { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent2, matchingComponent), true /* findExactPairMatch */,