From 7640d9742111d61a8716fd80ec9cf9c810f46dcb Mon Sep 17 00:00:00 2001 From: Uwais Ashraf Date: Tue, 27 Aug 2024 16:03:47 +0000 Subject: [PATCH] Make TasksRepository listen to changes in task visuals and update To do this TaskVisualsChangedDelegate was added which multiplexes two listeners that are closely related. Thumbnail overriding capabilities were also removed. Fix: 342560598 Test: TaskVisualsChangedDelegateTest, TasksRepositoryTest Flag: com.android.launcher3.enable_refactor_task_thumbnail Change-Id: If065e4179cd1f15fe2cdf9b6bae51afcb57abcc6 --- .../com/android/quickstep/RecentsModel.java | 6 +- .../android/quickstep/TaskThumbnailCache.java | 9 +- .../data/HighResLoadingStateNotifier.kt | 28 +++ .../recents/data/RecentTasksRepository.kt | 6 - .../recents/data/TaskVisualsChangeNotifier.kt | 28 +++ .../data/TaskVisualsChangedDelegate.kt | 145 +++++++++++++ .../quickstep/recents/data/TasksRepository.kt | 117 ++++++----- .../recents/di/RecentsDependencies.kt | 13 +- .../recents/viewmodel/RecentsViewModel.kt | 4 - .../util/TaskVisualsChangeListener.java | 3 +- .../android/quickstep/views/RecentsView.java | 5 +- .../quickstep/views/RecentsViewModelHelper.kt | 4 - .../data/FakeHighResLoadingStateNotifier.kt | 31 +++ .../recents/data/FakeTaskIconDataSource.kt | 23 ++- .../data/FakeTaskThumbnailDataSource.kt | 3 +- .../data/FakeTaskVisualsChangeNotifier.kt | 31 +++ .../recents/data/FakeTasksRepository.kt | 13 +- .../data/TaskVisualsChangedDelegateTest.kt | 191 ++++++++++++++++++ .../recents/data/TasksRepositoryTest.kt | 138 ++++++------- .../recents/viewmodel/RecentsViewModelTest.kt | 38 ---- 20 files changed, 633 insertions(+), 203 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt create mode 100644 quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt create mode 100644 quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index db03dac608..a01ceb2446 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -43,6 +43,7 @@ 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.recents.data.TaskVisualsChangeNotifier; import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.TaskVisualsChangeListener; @@ -65,7 +66,8 @@ import java.util.function.Predicate; */ @TargetApi(Build.VERSION_CODES.O) public class RecentsModel implements RecentTasksDataSource, IconChangeListener, - TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable { + TaskStackChangeListener, TaskVisualsChangeListener, TaskVisualsChangeNotifier, + SafeCloseable { // We do not need any synchronization for this variable as its only written on UI thread. public static final MainThreadInitializedObject INSTANCE = @@ -287,6 +289,7 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, /** * Adds a listener for visuals changes */ + @Override public void addThumbnailChangeListener(TaskVisualsChangeListener listener) { mThumbnailChangeListeners.add(listener); } @@ -294,6 +297,7 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, /** * Removes a previously added listener */ + @Override public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) { mThumbnailChangeListeners.remove(listener); } diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java index 3c6c3e4a94..580dcc2ad9 100644 --- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java +++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java @@ -27,6 +27,7 @@ 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.recents.data.HighResLoadingStateNotifier; import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource; import com.android.quickstep.util.TaskKeyByLastActiveTimeCache; import com.android.quickstep.util.TaskKeyCache; @@ -48,7 +49,7 @@ public class TaskThumbnailCache implements TaskThumbnailDataSource { private final boolean mEnableTaskSnapshotPreloading; private final Context mContext; - public static class HighResLoadingState { + public static class HighResLoadingState implements HighResLoadingStateNotifier { private boolean mForceHighResThumbnails; private boolean mVisible; private boolean mFlingingFast; @@ -65,11 +66,13 @@ public class TaskThumbnailCache implements TaskThumbnailDataSource { mForceHighResThumbnails = !supportsLowResThumbnails(); } - public void addCallback(HighResLoadingStateChangedCallback callback) { + @Override + public void addCallback(@NonNull HighResLoadingStateChangedCallback callback) { mCallbacks.add(callback); } - public void removeCallback(HighResLoadingStateChangedCallback callback) { + @Override + public void removeCallback(@NonNull HighResLoadingStateChangedCallback callback) { mCallbacks.remove(callback); } diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt new file mode 100644 index 0000000000..df546ca776 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt @@ -0,0 +1,28 @@ +/* + * 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.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback + +/** Notifies added callbacks that high res state has changed */ +interface HighResLoadingStateNotifier { + /** Adds a callback for high res loading state */ + fun addCallback(callback: HighResLoadingStateChangedCallback) + + /** Removes a callback for high res loading state */ + fun removeCallback(callback: HighResLoadingStateChangedCallback) +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt index 4f7a541fee..9c4248cec9 100644 --- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt +++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt @@ -41,10 +41,4 @@ interface RecentTasksRepository { * populated e.g. icons/thumbnails etc. */ fun setVisibleTasks(visibleTaskIdList: List) - - /** - * Override [ThumbnailData] with a map of taskId to [ThumbnailData]. The override only applies - * if the tasks are already visible, and will be invalidated when tasks become invisible. - */ - fun addOrUpdateThumbnailOverride(thumbnailOverride: Map) } diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt new file mode 100644 index 0000000000..6e7789d7f8 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt @@ -0,0 +1,28 @@ +/* + * 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.TaskVisualsChangeListener + +/** Notifies added listeners that task visuals have changed */ +interface TaskVisualsChangeNotifier { + /** Adds a listener for visuals changes */ + fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) + + /** Removes a listener for visuals changes */ + fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt new file mode 100644 index 0000000000..a141e89c23 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt @@ -0,0 +1,145 @@ +/* + * 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.os.UserHandle +import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.quickstep.util.TaskVisualsChangeListener +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData + +/** Delegates the checking of task visuals (thumbnails, high res changes, icons) */ +interface TaskVisualsChangedDelegate : + TaskVisualsChangeListener, HighResLoadingStateChangedCallback { + /** Registers a callback for visuals relating to icons */ + fun registerTaskIconChangedCallback( + taskKey: Task.TaskKey, + taskIconChangedCallback: TaskIconChangedCallback + ) + + /** Unregisters a callback for visuals relating to icons */ + fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) + + /** Registers a callback for visuals relating to thumbnails */ + fun registerTaskThumbnailChangedCallback( + taskKey: Task.TaskKey, + taskThumbnailChangedCallback: TaskThumbnailChangedCallback + ) + + /** Unregisters a callback for visuals relating to thumbnails */ + fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) + + /** A callback for task icon changes */ + interface TaskIconChangedCallback { + /** Informs the listener that the task icon has changed */ + fun onTaskIconChanged() + } + + /** A callback for task thumbnail changes */ + interface TaskThumbnailChangedCallback { + /** Informs the listener that the task thumbnail data has changed to [thumbnailData] */ + fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) + + /** Informs the listener that the default resolution for loading thumbnails has changed */ + fun onHighResLoadingStateChanged() + } +} + +class TaskVisualsChangedDelegateImpl( + private val taskVisualsChangeNotifier: TaskVisualsChangeNotifier, + private val highResLoadingStateNotifier: HighResLoadingStateNotifier, +) : TaskVisualsChangedDelegate { + private val taskIconChangedCallbacks = + mutableMapOf>() + private val taskThumbnailChangedCallbacks = + mutableMapOf>() + private var isListening = false + + @Synchronized + private fun onCallbackRegistered() { + if (isListening) return + + taskVisualsChangeNotifier.addThumbnailChangeListener(this) + highResLoadingStateNotifier.addCallback(this) + isListening = true + } + + @Synchronized + private fun onCallbackUnregistered() { + if (!isListening) return + + if (taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size == 0) { + taskVisualsChangeNotifier.removeThumbnailChangeListener(this) + highResLoadingStateNotifier.removeCallback(this) + } + + isListening = false + } + + override fun onTaskIconChanged(taskId: Int) { + taskIconChangedCallbacks[taskId]?.let { (_, callback) -> callback.onTaskIconChanged() } + } + + override fun onTaskIconChanged(pkg: String, user: UserHandle) { + taskIconChangedCallbacks.values + .filter { (taskKey, _) -> + pkg == taskKey.packageName && user.identifier == taskKey.userId + } + .forEach { (_, callback) -> callback.onTaskIconChanged() } + } + + override fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData?): Task? { + taskThumbnailChangedCallbacks[taskId]?.let { (_, callback) -> + callback.onTaskThumbnailChanged(thumbnailData) + } + return null + } + + override fun onHighResLoadingStateChanged(enabled: Boolean) { + taskThumbnailChangedCallbacks.values.forEach { (_, callback) -> + callback.onHighResLoadingStateChanged() + } + } + + override fun registerTaskIconChangedCallback( + taskKey: Task.TaskKey, + taskIconChangedCallback: TaskIconChangedCallback + ) { + taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback + onCallbackRegistered() + } + + override fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) { + taskIconChangedCallbacks.remove(taskKey.id) + onCallbackUnregistered() + } + + override fun registerTaskThumbnailChangedCallback( + taskKey: Task.TaskKey, + taskThumbnailChangedCallback: TaskThumbnailChangedCallback + ) { + taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback + onCallbackRegistered() + } + + override fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) { + taskThumbnailChangedCallbacks.remove(taskKey.id) + onCallbackUnregistered() + } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt index 6f9d157bd4..eb3c2d1217 100644 --- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt +++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt @@ -18,6 +18,8 @@ package com.android.quickstep.recents.data import android.graphics.drawable.Drawable import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +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 @@ -26,18 +28,20 @@ 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.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.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -46,12 +50,12 @@ class TasksRepository( private val recentsModel: RecentTasksDataSource, private val taskThumbnailDataSource: TaskThumbnailDataSource, private val taskIconDataSource: TaskIconDataSource, + private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate, recentsCoroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, ) : RecentTasksRepository { private val groupedTaskData = MutableStateFlow(emptyList()) private val visibleTaskIds = MutableStateFlow(emptySet()) - private val thumbnailOverride = MutableStateFlow(mapOf()) private val taskData = groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } } @@ -85,15 +89,13 @@ class TasksRepository( .distinctUntilChanged() private val augmentedTaskData: Flow> = - combine(taskData, thumbnailQueryResults, iconQueryResults, thumbnailOverride) { + combine(taskData, thumbnailQueryResults, iconQueryResults) { tasks, thumbnailQueryResults, - iconQueryResults, - thumbnailOverride -> + iconQueryResults -> tasks.onEach { task -> // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible) - task.thumbnail = - thumbnailOverride[task.key.id] ?: thumbnailQueryResults[task.key.id] + task.thumbnail = thumbnailQueryResults[task.key.id] // TODO(b/352331675) don't load icons for DesktopTaskView // Add retrieved icons + remove unnecessary icons @@ -121,61 +123,76 @@ class TasksRepository( override fun setVisibleTasks(visibleTaskIdList: List) { this.visibleTaskIds.value = visibleTaskIdList.toSet() - addOrUpdateThumbnailOverride(emptyMap()) - } - - override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map) { - this.thumbnailOverride.value = - this.thumbnailOverride.value - .toMutableMap() - .apply { putAll(thumbnailOverride) } - .filterKeys(this.visibleTaskIds.value::contains) } /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */ - private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = flow { - emit(task.key.id to task.thumbnail) - val thumbnailDataResult: ThumbnailData? = - withContext(dispatcherProvider.main) { - suspendCancellableCoroutine { continuation -> - val cancellableTask = - taskThumbnailDataSource.getThumbnailInBackground(task) { - continuation.resume(it) - } - continuation.invokeOnCancellation { cancellableTask?.cancel() } + private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow { + trySend(task.key.id to task.thumbnail) + trySend(task.key.id to getThumbnailFromDataSource(task)) + + val callback = + object : TaskThumbnailChangedCallback { + override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) { + trySend(task.key.id to thumbnailData) + } + + override fun onHighResLoadingStateChanged() { + launch { trySend(task.key.id to getThumbnailFromDataSource(task)) } } } - emit(task.key.id to thumbnailDataResult) + taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback) + awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) } } - /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */ + /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */ private fun getIconDataRequest(task: Task): IconDataRequest = - flow { - emit(task.key.id to task.getTaskIconQueryResponse()) - val iconDataResponse: TaskIconQueryResponse? = - withContext(dispatcherProvider.main) { - suspendCancellableCoroutine { continuation -> - val cancellableTask = - taskIconDataSource.getIconInBackground(task) { - icon, - contentDescription, - title -> - icon.constantState?.let { - continuation.resume( - TaskIconQueryResponse( - it.newDrawable().mutate(), - contentDescription, - title - ) - ) - } - } - continuation.invokeOnCancellation { cancellableTask?.cancel() } + 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)) } } } - emit(task.key.id to iconDataResponse) + taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback) + awaitClose { + taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key) + } } .distinctUntilChanged() + + private suspend fun getThumbnailFromDataSource(task: Task) = + withContext(dispatcherProvider.main) { + suspendCancellableCoroutine { continuation -> + val cancellableTask = + taskThumbnailDataSource.getThumbnailInBackground(task) { + continuation.resume(it) + } + continuation.invokeOnCancellation { cancellableTask?.cancel() } + } + } + + private suspend fun getIconFromDataSource(task: Task) = + withContext(dispatcherProvider.main) { + suspendCancellableCoroutine { continuation -> + val cancellableTask = + taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title + -> + icon.constantState?.let { + continuation.resume( + TaskIconQueryResponse( + it.newDrawable().mutate(), + contentDescription, + title + ) + ) + } + } + continuation.invokeOnCancellation { cancellableTask?.cancel() } + } + } } data class TaskIconQueryResponse( diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt index 0b672d1b94..0a5544f36b 100644 --- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt @@ -22,6 +22,8 @@ import android.view.View import com.android.launcher3.util.coroutines.ProductionDispatchers import com.android.quickstep.RecentsModel import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate +import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl import com.android.quickstep.recents.data.TasksRepository import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase import com.android.quickstep.recents.usecase.GetThumbnailUseCase @@ -60,14 +62,22 @@ class RecentsDependencies private constructor(private val appContext: Context) { val recentsCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView")) set(CoroutineScope::class.java.simpleName, recentsCoroutineScope) + val recentsModel = RecentsModel.INSTANCE.get(appContext) + val taskVisualsChangedDelegate = + TaskVisualsChangedDelegateImpl( + recentsModel, + recentsModel.thumbnailCache.highResLoadingState + ) + set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate) // Create RecentsTaskRepository singleton val recentTasksRepository: RecentTasksRepository = - with(RecentsModel.INSTANCE.get(appContext)) { + with(recentsModel) { TasksRepository( this, thumbnailCache, iconCache, + taskVisualsChangedDelegate, recentsCoroutineScope, ProductionDispatchers ) @@ -155,6 +165,7 @@ class RecentsDependencies private constructor(private val appContext: Context) { thumbnailCache, iconCache, get(), + get(), ProductionDispatchers ) } diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt index b1f46a37f4..1716f2e2dc 100644 --- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt +++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt @@ -58,10 +58,6 @@ class RecentsViewModel( recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha } - fun addOrUpdateThumbnailOverride(thumbnailOverride: Map) { - recentsTasksRepository.addOrUpdateThumbnailOverride(thumbnailOverride) - } - suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map) { combine( updatedThumbnails.map { diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java index 66bff730bf..519ef60eca 100644 --- a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java +++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; +import android.annotation.NonNull; import android.os.UserHandle; import com.android.systemui.shared.recents.model.Task; @@ -36,7 +37,7 @@ public interface TaskVisualsChangeListener { /** * Called when the icon for a task changes */ - default void onTaskIconChanged(String pkg, UserHandle user) {} + default void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {} /** * Called when the icon for a task changes diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 69a9690321..255619ae9d 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -1079,7 +1079,6 @@ public abstract class RecentsView< @Nullable public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) { if (enableRefactorTaskThumbnail()) { - mHelper.onTaskThumbnailChanged(taskId, thumbnailData); return null; } if (mHandleTaskStackChanges) { @@ -1100,8 +1099,7 @@ public abstract class RecentsView< } @Override - public void onTaskIconChanged(String pkg, UserHandle user) { - // TODO(b/342560598): Listen in TaskRepository and reload. + public void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) { for (int i = 0; i < getTaskViewCount(); i++) { TaskView tv = requireTaskViewAt(i); Task task = tv.getFirstTask(); @@ -2549,7 +2547,6 @@ public abstract class RecentsView< } if (enableRefactorTaskThumbnail()) { - // TODO(b/342560598): Listen in TaskRepository and reload. return; } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt index 5b71da10b0..f5b217648a 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt +++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt @@ -68,8 +68,4 @@ class RecentsViewModelHelper(private val recentsViewModel: RecentsViewModel) { ViewUtils.postFrameDrawn(taskView, onFinishRunnable) } } - - fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData) { - recentsViewModel.addOrUpdateThumbnailOverride(mapOf(taskId to thumbnailData)) - } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt new file mode 100644 index 0000000000..7d09efd100 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt @@ -0,0 +1,31 @@ +/* + * 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.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback + +class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier { + val listeners = mutableListOf() + + override fun addCallback(callback: HighResLoadingStateChangedCallback) { + listeners.add(callback) + } + + override fun removeCallback(callback: HighResLoadingStateChangedCallback) { + listeners.remove(callback) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt index fee4979465..5de876a525 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt @@ -27,7 +27,8 @@ import org.mockito.kotlin.whenever class FakeTaskIconDataSource : TaskIconDataSource { - val taskIdToDrawable: Map = (0..10).associateWith { mockCopyableDrawable() } + val taskIdToDrawable: MutableMap = + (0..10).associateWith { mockCopyableDrawable() }.toMutableMap() val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() var shouldLoadSynchronously: Boolean = true @@ -52,15 +53,17 @@ class FakeTaskIconDataSource : TaskIconDataSource { return null } - private fun mockCopyableDrawable(): Drawable { - val mutableDrawable = mock() - val immutableDrawable = - mock().apply { whenever(mutate()).thenReturn(mutableDrawable) } - val constantState = - mock().apply { - whenever(newDrawable()).thenReturn(immutableDrawable) - } - return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) } + companion object { + fun mockCopyableDrawable(): Drawable { + val mutableDrawable = mock() + val immutableDrawable = + mock().apply { whenever(mutate()).thenReturn(mutableDrawable) } + val constantState = + mock().apply { + whenever(newDrawable()).thenReturn(immutableDrawable) + } + return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) } + } } } 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 index 30fc4916a8..d12c0b0a5a 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt @@ -27,7 +27,8 @@ import org.mockito.kotlin.whenever class FakeTaskThumbnailDataSource : TaskThumbnailDataSource { - val taskIdToBitmap: Map = (0..10).associateWith { mock() } + val taskIdToBitmap: MutableMap = + (0..10).associateWith { mock() }.toMutableMap() val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() var shouldLoadSynchronously: Boolean = true diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt new file mode 100644 index 0000000000..765f0d1dfb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt @@ -0,0 +1,31 @@ +/* + * 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.TaskVisualsChangeListener + +class FakeTaskVisualsChangeNotifier : TaskVisualsChangeNotifier { + val listeners = mutableListOf() + + override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) { + listeners.add(listener) + } + + override fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) { + listeners.remove(listener) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt index d94a351e87..7a17872827 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt @@ -28,7 +28,6 @@ class FakeTasksRepository : RecentTasksRepository { private var taskIconDataMap: Map = emptyMap() private var tasks: MutableStateFlow> = MutableStateFlow(emptyList()) private var visibleTasks: MutableStateFlow> = MutableStateFlow(emptyList()) - private var thumbnailOverrideMap: Map = emptyMap() override fun getAllTaskData(forceRefresh: Boolean): Flow> = tasks @@ -39,7 +38,7 @@ class FakeTasksRepository : RecentTasksRepository { .map { taskList -> val task = taskList.firstOrNull { it.key.id == taskId } ?: return@map null Task(task).apply { - thumbnail = thumbnailOverrideMap[taskId] ?: task.thumbnail + thumbnail = task.thumbnail icon = task.icon titleDescription = task.titleDescription title = task.title @@ -62,16 +61,6 @@ class FakeTasksRepository : RecentTasksRepository { } } } - setThumbnailOverrideInternal(thumbnailOverrideMap) - } - - override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map) { - setThumbnailOverrideInternal(thumbnailOverride) - } - - private fun setThumbnailOverrideInternal(thumbnailOverride: Map) { - thumbnailOverrideMap = - thumbnailOverride.filterKeys(this.visibleTasks.value::contains).toMap() } fun seedTasks(tasks: List) { diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt new file mode 100644 index 0000000000..41f6bfdf75 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt @@ -0,0 +1,191 @@ +/* + * 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 android.os.UserHandle +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions + +class TaskVisualsChangedDelegateTest { + private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier() + private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier() + + val systemUnderTest = + TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier) + + @Test + fun addingFirstListener_addsListenerToNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + + assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest) + assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest) + } + + @Test + fun addingAndRemovingListener_removesListenerFromNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1)) + + assertThat(taskVisualsChangeNotifier.listeners).isEmpty() + assertThat(highResLoadingStateNotifier.listeners).isEmpty() + } + + @Test + fun addingTwoAndRemovingOneListener_doesNotRemoveListenerFromNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 2), mock()) + systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1)) + + assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest) + assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest) + } + + @Test + fun onTaskIconChangedWithTaskId_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 1), expectedListener) + systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 2), additionalListener) + + systemUnderTest.onTaskIconChanged(1) + + verify(expectedListener).onTaskIconChanged() + verifyNoMoreInteractions(additionalListener) + } + + @Test + fun onTaskIconChangedWithoutTaskId_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val listener = mock() + // Correct match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + expectedListener + ) + // 1 out of 2 match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1), + listener + ) + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2), + listener + ) + // 0 out of 2 match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2), + listener + ) + + systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1)) + + verify(expectedListener).onTaskIconChanged() + verifyNoMoreInteractions(listener) + } + + @Test + fun replacedTaskIconChangedCallbacks_notCalled() { + val replacedListener = mock() + val newListener = mock() + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + replacedListener + ) + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + newListener + ) + + systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1)) + + verifyNoMoreInteractions(replacedListener) + verify(newListener).onTaskIconChanged() + } + + @Test + fun onTaskThumbnailChanged_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val additionalListener = mock() + val expectedThumbnailData = ThumbnailData(snapshotId = 12345) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener + ) + + systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData) + + verify(expectedListener).onTaskThumbnailChanged(expectedThumbnailData) + verifyNoMoreInteractions(additionalListener) + } + + @Test + fun onHighResLoadingStateChanged_notifiesAllListeners() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener + ) + + systemUnderTest.onHighResLoadingStateChanged(true) + + verify(expectedListener).onHighResLoadingStateChanged() + verify(additionalListener).onHighResLoadingStateChanged() + } + + @Test + fun replacedTaskThumbnailChangedCallbacks_notCalled() { + val replacedListener1 = mock() + val newListener1 = mock() + val expectedThumbnailData = ThumbnailData(snapshotId = 12345) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + replacedListener1 + ) + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1) + + systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData) + + verifyNoMoreInteractions(replacedListener1) + verify(newListener1).onTaskThumbnailChanged(expectedThumbnailData) + } + + private fun createTaskKey(id: Int = 1, pkg: String = PACKAGE_NAME, userId: Int = 1) = + TaskKey(id, 0, Intent().setPackage(pkg), ComponentName("", ""), userId, 0) + + private companion object { + const val PACKAGE_NAME = "com.test.test" + const val ALTERNATIVE_PACKAGE_NAME = "com.test.test2" + } +} 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 index e6534ebeea..f31467f06c 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt @@ -19,7 +19,7 @@ package com.android.quickstep.recents.data import android.content.ComponentName import android.content.Intent import android.graphics.Bitmap -import android.view.Surface +import android.graphics.drawable.Drawable import com.android.launcher3.util.TestDispatcherProvider import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest import com.android.quickstep.util.DesktopTask @@ -29,6 +29,9 @@ import com.android.systemui.shared.recents.model.ThumbnailData import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -48,6 +51,10 @@ class TasksRepositoryTest { private val recentsModel = FakeRecentTasksDataSource() private val taskThumbnailDataSource = FakeTaskThumbnailDataSource() private val taskIconDataSource = FakeTaskIconDataSource() + private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier() + private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier() + private val taskVisualsChangedDelegate = + TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier) private val dispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(dispatcher) @@ -56,6 +63,7 @@ class TasksRepositoryTest { recentsModel, taskThumbnailDataSource, taskIconDataSource, + taskVisualsChangedDelegate, testScope.backgroundScope, TestDispatcherProvider(dispatcher) ) @@ -203,87 +211,81 @@ class TasksRepositoryTest { } @Test - fun addThumbnailOverrideOverrideThumbnails() = + fun onTaskThumbnailChanged_setsNewThumbnailDataOnTask() = testScope.runTest { recentsModel.seedTasks(defaultTaskList) - val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] - val thumbnailOverride2 = createThumbnailData() - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2)) - - assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) - assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail) - .isEqualTo(thumbnailOverride2.thumbnail) - } - - @Test - fun addThumbnailOverrideMultipleOverrides() = - testScope.runTest { - recentsModel.seedTasks(defaultTaskList) - val thumbnailOverride1 = createThumbnailData() - val thumbnailOverride2 = createThumbnailData() - val thumbnailOverride3 = createThumbnailData() - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1)) - systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2)) - systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride3)) - - assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail) - .isEqualTo(thumbnailOverride1.thumbnail) - assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail) - .isEqualTo(thumbnailOverride3.thumbnail) - } - - @Test - fun addThumbnailOverrideClearedWhenTaskBecomeInvisible() = - testScope.runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - val thumbnailOverride1 = createThumbnailData() - val thumbnailOverride2 = createThumbnailData() - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - systemUnderTest.addOrUpdateThumbnailOverride(mapOf(1 to thumbnailOverride1)) - systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride2)) - // Making task 2 invisible and visible again should clear the override - systemUnderTest.setVisibleTasks(listOf(1)) - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail) - .isEqualTo(thumbnailOverride1.thumbnail) - assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2) - } - - @Test - fun addThumbnailOverrideDoesNotOverrideInvisibleTasks() = - testScope.runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - val thumbnailOverride = createThumbnailData() systemUnderTest.getAllTaskData(forceRefresh = true) systemUnderTest.setVisibleTasks(listOf(1)) - systemUnderTest.addOrUpdateThumbnailOverride(mapOf(2 to thumbnailOverride)) - systemUnderTest.setVisibleTasks(listOf(1, 2)) - assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) - assertThat(systemUnderTest.getThumbnailById(2).first()!!.thumbnail).isEqualTo(bitmap2) + val expectedThumbnailData = createThumbnailData() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues) + } + taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData) + + assertThat(task1ThumbnailValues[1]!!.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData) + } + + @Test + fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1)) + + val expectedBitmap = mock() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues) + } + taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap + taskVisualsChangedDelegate.onHighResLoadingStateChanged(true) + + assertThat(task1ThumbnailValues[1]).isEqualTo(expectedPreviousBitmap) + assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap) + } + + @Test + fun onTaskIconChanged_setsNewIconOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(forceRefresh = true) + + systemUnderTest.setVisibleTasks(listOf(1)) + + val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable() + val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1IconValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.icon }.toList(task1IconValues) + } + taskIconDataSource.taskIdToDrawable[1] = expectedIcon + taskVisualsChangedDelegate.onTaskIconChanged(1) + + assertThat(task1IconValues[1]).isEqualTo(expectedPreviousIcon) + assertThat(task1IconValues.last()).isEqualTo(expectedIcon) } private fun createTaskWithId(taskId: Int) = Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)) - private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData { + private fun createThumbnailData(): ThumbnailData { val bitmap = mock() whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH) whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT) - return ThumbnailData(thumbnail = bitmap, rotation = rotation) + return ThumbnailData(thumbnail = bitmap) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt index dc16475e0c..fe67313329 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt @@ -70,44 +70,6 @@ class RecentsViewModelTest { assertThat(thumbnailDataFlow2.first()).isNull() } - @Test - fun thumbnailOverrideWaitAndReset() = runTest { - val thumbnailData1 = createThumbnailData().apply { snapshotId = 1 } - val thumbnailData2 = createThumbnailData().apply { snapshotId = 2 } - tasksRepository.seedTasks(tasks) - tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2)) - - val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1) - val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2) - - systemUnderTest.refreshAllTaskData() - systemUnderTest.updateVisibleTasks(listOf(1, 2)) - - assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) - assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2) - - systemUnderTest.setRunningTaskShowScreenshot(true) - val thumbnailOverride = mapOf(2 to createThumbnailData().apply { snapshotId = 3 }) - systemUnderTest.addOrUpdateThumbnailOverride(thumbnailOverride) - - systemUnderTest.waitForRunningTaskShowScreenshotToUpdate() - val expectedUpdate = mapOf(2 to createThumbnailData().apply { snapshotId = 3 }) - systemUnderTest.waitForThumbnailsToUpdate(expectedUpdate) - - assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) - assertThat(thumbnailDataFlow2.first()?.snapshotId).isEqualTo(3) - - systemUnderTest.onReset() - - assertThat(thumbnailDataFlow1.first()).isNull() - assertThat(thumbnailDataFlow2.first()).isNull() - - systemUnderTest.updateVisibleTasks(listOf(1, 2)) - - assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) - assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2) - } - private fun createTaskWithId(taskId: Int) = Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { colorBackground = Color.argb(taskId, taskId, taskId, taskId)