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)