Merge "Make TasksRepository listen to changes in task visuals and update" into main

This commit is contained in:
Treehugger Robot
2024-08-28 22:31:22 +00:00
committed by Android (Google) Code Review
20 changed files with 633 additions and 203 deletions
@@ -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<RecentsModel> 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);
}
@@ -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);
}
@@ -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)
}
@@ -41,10 +41,4 @@ interface RecentTasksRepository {
* populated e.g. icons/thumbnails etc.
*/
fun setVisibleTasks(visibleTaskIdList: List<Int>)
/**
* 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<Int, ThumbnailData>)
}
@@ -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)
}
@@ -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<Int, Pair<Task.TaskKey, TaskIconChangedCallback>>()
private val taskThumbnailChangedCallbacks =
mutableMapOf<Int, Pair<Task.TaskKey, TaskThumbnailChangedCallback>>()
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()
}
}
@@ -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<GroupTask>())
private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
private val thumbnailOverride = MutableStateFlow(mapOf<Int, ThumbnailData>())
private val taskData =
groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
@@ -85,15 +89,13 @@ class TasksRepository(
.distinctUntilChanged()
private val augmentedTaskData: Flow<List<Task>> =
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<Int>) {
this.visibleTaskIds.value = visibleTaskIdList.toSet()
addOrUpdateThumbnailOverride(emptyMap())
}
override fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
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(
@@ -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
)
}
@@ -58,10 +58,6 @@ class RecentsViewModel(
recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
}
fun addOrUpdateThumbnailOverride(thumbnailOverride: Map<Int, ThumbnailData>) {
recentsTasksRepository.addOrUpdateThumbnailOverride(thumbnailOverride)
}
suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>) {
combine(
updatedThumbnails.map {
@@ -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
@@ -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;
}
@@ -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))
}
}
@@ -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<HighResLoadingStateChangedCallback>()
override fun addCallback(callback: HighResLoadingStateChangedCallback) {
listeners.add(callback)
}
override fun removeCallback(callback: HighResLoadingStateChangedCallback) {
listeners.remove(callback)
}
}
@@ -27,7 +27,8 @@ import org.mockito.kotlin.whenever
class FakeTaskIconDataSource : TaskIconDataSource {
val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mockCopyableDrawable() }
val taskIdToDrawable: MutableMap<Int, Drawable> =
(0..10).associateWith { mockCopyableDrawable() }.toMutableMap()
val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
var shouldLoadSynchronously: Boolean = true
@@ -52,15 +53,17 @@ class FakeTaskIconDataSource : TaskIconDataSource {
return null
}
private fun mockCopyableDrawable(): Drawable {
val mutableDrawable = mock<Drawable>()
val immutableDrawable =
mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
val constantState =
mock<Drawable.ConstantState>().apply {
whenever(newDrawable()).thenReturn(immutableDrawable)
}
return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
companion object {
fun mockCopyableDrawable(): Drawable {
val mutableDrawable = mock<Drawable>()
val immutableDrawable =
mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
val constantState =
mock<Drawable.ConstantState>().apply {
whenever(newDrawable()).thenReturn(immutableDrawable)
}
return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
}
}
}
@@ -27,7 +27,8 @@ import org.mockito.kotlin.whenever
class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
val taskIdToBitmap: Map<Int, Bitmap> = (0..10).associateWith { mock() }
val taskIdToBitmap: MutableMap<Int, Bitmap> =
(0..10).associateWith { mock<Bitmap>() }.toMutableMap()
val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
var shouldLoadSynchronously: Boolean = true
@@ -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<TaskVisualsChangeListener>()
override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) {
listeners.add(listener)
}
override fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) {
listeners.remove(listener)
}
}
@@ -28,7 +28,6 @@ class FakeTasksRepository : RecentTasksRepository {
private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
private var thumbnailOverrideMap: Map<Int, ThumbnailData> = emptyMap()
override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = 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<Int, ThumbnailData>) {
setThumbnailOverrideInternal(thumbnailOverride)
}
private fun setThumbnailOverrideInternal(thumbnailOverride: Map<Int, ThumbnailData>) {
thumbnailOverrideMap =
thumbnailOverride.filterKeys(this.visibleTasks.value::contains).toMap()
}
fun seedTasks(tasks: List<Task>) {
@@ -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<TaskIconChangedCallback>()
val additionalListener = mock<TaskIconChangedCallback>()
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<TaskIconChangedCallback>()
val listener = mock<TaskIconChangedCallback>()
// 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<TaskIconChangedCallback>()
val newListener = mock<TaskIconChangedCallback>()
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<TaskThumbnailChangedCallback>()
val additionalListener = mock<TaskThumbnailChangedCallback>()
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<TaskThumbnailChangedCallback>()
val additionalListener = mock<TaskThumbnailChangedCallback>()
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<TaskThumbnailChangedCallback>()
val newListener1 = mock<TaskThumbnailChangedCallback>()
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"
}
}
@@ -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<ThumbnailData?>()
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<Bitmap>()
val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
val taskDataFlow = systemUnderTest.getTaskDataById(1)
val task1ThumbnailValues = mutableListOf<Bitmap?>()
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<Drawable?>()
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<Bitmap>()
whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH)
whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT)
return ThumbnailData(thumbnail = bitmap, rotation = rotation)
return ThumbnailData(thumbnail = bitmap)
}
}
@@ -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)