diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index 2b0e1699cc..365c5c49e0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -15,9 +15,6 @@ */ package com.android.launcher3.taskbar; -import static com.android.window.flags.Flags.enableDesktopWindowingMode; -import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps; - import android.util.SparseArray; import android.view.View; @@ -29,7 +26,6 @@ import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; @@ -37,8 +33,6 @@ import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; -import com.android.quickstep.LauncherActivityInterface; -import com.android.quickstep.RecentsModel; import java.io.PrintWriter; import java.util.ArrayList; @@ -54,7 +48,7 @@ import java.util.function.Predicate; * Launcher model Callbacks for rendering taskbar. */ public class TaskbarModelCallbacks implements - BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener { + BgDataModel.Callbacks, LauncherBindableItemsContainer { private final SparseArray mHotseatItems = new SparseArray<>(); private List mPredictedItems = Collections.emptyList(); @@ -68,8 +62,6 @@ public class TaskbarModelCallbacks implements // Used to defer any UI updates during the SUW unstash animation. private boolean mDeferUpdatesForSUW; private Runnable mDeferredUpdates; - private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener = - visible -> updateRunningApps(); public TaskbarModelCallbacks( TaskbarActivityContext context, TaskbarView container) { @@ -79,39 +71,6 @@ public class TaskbarModelCallbacks implements public void init(TaskbarControllers controllers) { mControllers = controllers; - if (mControllers.taskbarRecentAppsController.getCanShowRunningApps()) { - RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this); - - if (shouldShowRunningAppsInDesktopMode()) { - DesktopVisibilityController desktopVisibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (desktopVisibilityController != null) { - desktopVisibilityController.registerDesktopVisibilityListener( - mDesktopVisibilityListener); - } - } - } - } - - /** - * Unregisters listeners in this class. - */ - public void unregisterListeners() { - RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener(); - - if (shouldShowRunningAppsInDesktopMode()) { - DesktopVisibilityController desktopVisibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (desktopVisibilityController != null) { - desktopVisibilityController.unregisterDesktopVisibilityListener( - mDesktopVisibilityListener); - } - } - } - - private boolean shouldShowRunningAppsInDesktopMode() { - // TODO(b/335401172): unify DesktopMode checks in Launcher - return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps(); } @Override @@ -232,10 +191,12 @@ public class TaskbarModelCallbacks implements predictionNextIndex++; } } - hotseatItemInfos = mControllers.taskbarRecentAppsController - .updateHotseatItemInfos(hotseatItemInfos); - Set runningPackages = mControllers.taskbarRecentAppsController.getRunningApps(); - Set minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps(); + + final TaskbarRecentAppsController recentAppsController = + mControllers.taskbarRecentAppsController; + hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos); + Set runningPackages = recentAppsController.getRunningAppPackages(); + Set minimizedPackages = recentAppsController.getMinimizedAppPackages(); if (mDeferUpdatesForSUW) { ItemInfo[] finalHotseatItemInfos = hotseatItemInfos; @@ -270,21 +231,11 @@ public class TaskbarModelCallbacks implements } } - @Override - public void onRunningTasksChanged() { - updateRunningApps(); - } - /** Called when there's a change in running apps to update the UI. */ public void commitRunningAppsToUI() { commitItemsToUI(); } - /** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */ - public void updateRunningApps() { - mControllers.taskbarRecentAppsController.updateRunningApps(); - } - @Override public void bindDeepShortcutMap(HashMap deepShortcutMapCopy) { mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy); @@ -296,7 +247,6 @@ public class TaskbarModelCallbacks implements Map packageUserKeytoUidMap) { Preconditions.assertUIThread(); mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap); - mControllers.taskbarRecentAppsController.setApps(apps); } protected void dumpLogs(String prefix, PrintWriter pw) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt index b1fc9ccb02..4866e399ac 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt @@ -15,16 +15,13 @@ */ package com.android.launcher3.taskbar -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration import androidx.annotation.VisibleForTesting -import com.android.launcher3.Flags.enableRecentsInTaskbar -import com.android.launcher3.model.data.AppInfo import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.statehandlers.DesktopVisibilityController import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController import com.android.quickstep.RecentsModel +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask import com.android.window.flags.Flags.enableDesktopWindowingMode import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps import java.io.PrintWriter @@ -42,22 +39,22 @@ class TaskbarRecentAppsController( ) : LoggableTaskbarController { // TODO(b/335401172): unify DesktopMode checks in Launcher. - val canShowRunningApps = + var canShowRunningApps = enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps() - - // TODO(b/343532825): Add a setting to disable Recents even when the flag is on. - var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps @VisibleForTesting - set(isEnabledFromTest){ + set(isEnabledFromTest) { field = isEnabledFromTest } // Initialized in init. private lateinit var controllers: TaskbarControllers - private var apps: Array? = null - private var allRunningDesktopAppInfos: List? = null - private var allMinimizedDesktopAppInfos: List? = null + private var shownHotseatItems: List = emptyList() + private var allRecentTasks: List = emptyList() + private var desktopTask: DesktopTask? = null + // TODO(next CL): actually read and show these + var shownTasks: List = emptyList() + private set private val desktopVisibilityController: DesktopVisibilityController? get() = desktopVisibilityControllerProvider() @@ -65,122 +62,132 @@ class TaskbarRecentAppsController( private val isInDesktopMode: Boolean get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false - val runningApps: Set + val runningAppPackages: Set + /** + * Returns the package names of apps that should be indicated as "running" to the user. + * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet(). + */ get() { - if (!isEnabled || !isInDesktopMode) { + if (!canShowRunningApps || !isInDesktopMode) { return emptySet() } - return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet() + val tasks = desktopTask?.tasks ?: return emptySet() + return tasks.map { task -> task.key.packageName }.toSet() } - val minimizedApps: Set + val minimizedAppPackages: Set + /** + * Returns the package names of apps that should be indicated as "minimized" to the user. + * Specifically, we return all the running packages where all the tasks in that package are + * minimized (not visible). + */ get() { - if (!isInDesktopMode) { + if (!canShowRunningApps || !isInDesktopMode) { return emptySet() } - return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() - ?: emptySet() + val desktopTasks = desktopTask?.tasks ?: return emptySet() + val packageToTasks = desktopTasks.groupBy { it.key.packageName } + return packageToTasks.filterValues { tasks -> tasks.all { !it.isVisible } }.keys } + private val recentTasksChangedListener = + RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() } + + // TODO(b/343291428): add TaskVisualsChangListener as well (for calendar/clock?) + + // Used to keep track of the last requested task list ID, so that we do not request to load the + // tasks again if we have already requested it and the task list has not changed + private var taskListChangeId = -1 + fun init(taskbarControllers: TaskbarControllers) { controllers = taskbarControllers + recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener) + reloadRecentTasksIfNeeded() } fun onDestroy() { - apps = null - } - - /** Stores the current [AppInfo] instances, no-op except in desktop environment. */ - fun setApps(apps: Array?) { - this.apps = apps + recentsModel.unregisterRecentTasksChangedListener() } /** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */ - // TODO(next CL): add new section of Tasks instead of changing Hotseat items fun updateHotseatItemInfos(hotseatItems: Array): Array { - if (!isEnabled || !isInDesktopMode) { + // Ignore predicted apps - we show running or recent apps instead. + val removePredictions = isInDesktopMode && canShowRunningApps + if (!removePredictions) { + shownHotseatItems = hotseatItems.filterNotNull() + onRecentsOrHotseatChanged() return hotseatItems } - val newHotseatItemInfos = + shownHotseatItems = hotseatItems .filterNotNull() - // Ignore predicted apps - we show running apps instead .filter { itemInfo -> !itemInfo.isPredictedItem } .toMutableList() - val runningDesktopAppInfos = - allRunningDesktopAppInfos?.let { - getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList()) + + onRecentsOrHotseatChanged() + + return shownHotseatItems.toTypedArray() + } + + private fun reloadRecentTasksIfNeeded() { + if (!recentsModel.isTaskListValid(taskListChangeId)) { + taskListChangeId = + recentsModel.getTasks { tasks -> + allRecentTasks = tasks + desktopTask = allRecentTasks.filterIsInstance().firstOrNull() + onRecentsOrHotseatChanged() + controllers.taskbarViewController.commitRunningAppsToUI() + } + } + } + + private fun onRecentsOrHotseatChanged() { + shownTasks = + if (isInDesktopMode) { + computeShownRunningTasks() + } else { + computeShownRecentTasks() } - if (runningDesktopAppInfos != null) { - newHotseatItemInfos.addAll(runningDesktopAppInfos) - } - return newHotseatItemInfos.toTypedArray() } - private fun getRunningDesktopAppInfosExceptHotseatApps( - allRunningDesktopAppInfos: List, - hotseatItems: List - ): List { - val hotseatPackages = hotseatItems.map { it.targetPackage } - return allRunningDesktopAppInfos - .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) } - .map { WorkspaceItemInfo(it) } - } - - private fun getDesktopRunningTasks(): List = - recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo -> - taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM - } - - // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos - private fun getAppInfosFromRunningTasks(tasks: List): List { - // Early return if apps is empty, since we then have no AppInfo to compare to - if (apps == null) { + private fun computeShownRunningTasks(): List { + if (!canShowRunningApps) { return emptyList() } - val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull() - return packageNames - .map { packageName -> apps?.find { app -> packageName == app.targetPackage } } - .filterNotNull() + val tasks = desktopTask?.tasks ?: emptyList() + // Kind of hacky, we wrap each single task in the Desktop as a GroupTask. + val desktopTaskAsList = tasks.map { GroupTask(it) } + // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too. + return dedupeHotseatTasks(desktopTaskAsList, shownHotseatItems) } - /** Called to update the list of currently running apps, no-op except in desktop environment. */ - fun updateRunningApps() { - if (!isEnabled || !isInDesktopMode) { - return controllers.taskbarViewController.commitRunningAppsToUI() + private fun computeShownRecentTasks(): List { + // TODO(next CL): implement Recents section + return emptyList() + } + + private fun dedupeHotseatTasks( + groupTasks: List, + shownHotseatItems: List + ): List { + val hotseatPackages = shownHotseatItems.map { item -> item.targetPackage } + return groupTasks.filter { groupTask -> + groupTask.hasMultipleTasks() || + !hotseatPackages.contains(groupTask.task1.key.packageName) } - val runningTasks = getDesktopRunningTasks() - val runningAppInfo = getAppInfosFromRunningTasks(runningTasks) - allRunningDesktopAppInfos = runningAppInfo - updateMinimizedApps(runningTasks, runningAppInfo) - controllers.taskbarViewController.commitRunningAppsToUI() - } - - private fun updateMinimizedApps( - runningTasks: List, - runningAppInfo: List, - ) { - val allRunningAppTasks = - runningAppInfo - .mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } } - .associate { (appInfo, targetPackage) -> - appInfo to - runningTasks - .filter { it.realActivity?.packageName == targetPackage } - .map { it.taskId } - } - val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible } - allMinimizedDesktopAppInfos = - allRunningAppTasks - .filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } } - .keys - .toList() } override fun dumpLogs(prefix: String, pw: PrintWriter) { pw.println("$prefix TaskbarRecentAppsController:") - pw.println("$prefix\tisEnabled=$isEnabled") pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps") - // TODO(next CL): add more logs + pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}") + pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}") + pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}") + pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}") + pw.println("$prefix\trunningTasks=$runningAppPackages") + pw.println("$prefix\tminimizedTasks=$minimizedAppPackages") } + + private val GroupTask.packageNames: List + get() = tasks.map { task -> task.key.packageName } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 55745b557a..b03894de25 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -224,7 +224,6 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks); mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener); - mModelCallbacks.unregisterListeners(); } public boolean areIconsVisible() { @@ -869,6 +868,11 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar return mTaskbarView.isEventOverAnyItem(ev); } + /** Called when there's a change in running apps to update the UI. */ + public void commitRunningAppsToUI() { + mModelCallbacks.commitRunningAppsToUI(); + } + @Override public void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "TaskbarViewController:"); @@ -889,14 +893,4 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar mModelCallbacks.dumpLogs(prefix + "\t", pw); } - /** Called when there's a change in running apps to update the UI. */ - public void commitRunningAppsToUI() { - mModelCallbacks.commitRunningAppsToUI(); - } - - /** Call TaskbarModelCallbacks to update running apps. */ - public void updateRunningApps() { - mModelCallbacks.updateRunningApps(); - } - } diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt index 1ec075ffaa..a391c68566 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt @@ -19,7 +19,6 @@ package com.android.launcher3.taskbar.customization import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.taskbar.TaskbarControllers -import com.android.launcher3.taskbar.TaskbarRecentAppsController import com.android.launcher3.util.DisplayController /** Evaluates all the features taskbar can have. */ @@ -34,7 +33,7 @@ class TaskbarFeatureEvaluator( val hasNavButtons = taskbarActivityContext.isThreeButtonNav val hasRecents: Boolean - get() = taskbarControllers.taskbarRecentAppsController.isEnabled + get() = taskbarControllers.taskbarRecentAppsController.shownTasks.isNotEmpty() val hasDivider: Boolean get() = enableTaskbarPinning() || hasRecents diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java index b08a46f3c2..66091d43f1 100644 --- a/quickstep/src/com/android/quickstep/RecentTasksList.java +++ b/quickstep/src/com/android/quickstep/RecentTasksList.java @@ -70,7 +70,8 @@ public class RecentTasksList { private TaskLoadResult mResultsBg = INVALID_RESULT; private TaskLoadResult mResultsUi = INVALID_RESULT; - private RecentsModel.RunningTasksListener mRunningTasksListener; + private @Nullable RecentsModel.RunningTasksListener mRunningTasksListener; + private @Nullable RecentsModel.RecentTasksChangedListener mRecentTasksChangedListener; // Tasks are stored in order of least recently launched to most recently launched. private ArrayList mRunningTasks; @@ -199,6 +200,9 @@ public class RecentTasksList { public void onRecentTasksChanged() { invalidateLoadedTasks(); + if (mRecentTasksChangedListener != null) { + mRecentTasksChangedListener.onRecentTasksChanged(); + } } private synchronized void invalidateLoadedTasks() { @@ -221,6 +225,21 @@ public class RecentTasksList { mRunningTasksListener = null; } + /** + * Registers a listener for running tasks + */ + public void registerRecentTasksChangedListener( + RecentsModel.RecentTasksChangedListener listener) { + mRecentTasksChangedListener = listener; + } + + /** + * Removes the previously registered running tasks listener + */ + public void unregisterRecentTasksChangedListener() { + mRecentTasksChangedListener = null; + } + private void initRunningTasks(ArrayList runningTasks) { // Tasks are retrieved in order of most recently launched/used to least recently launched. mRunningTasks = new ArrayList<>(runningTasks); @@ -357,6 +376,7 @@ public class RecentTasksList { task.setLastSnapshotData(taskInfo); task.positionInParent = taskInfo.positionInParent; task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds(); + task.isVisible = taskInfo.isVisible; tasks.add(task); } return new DesktopTask(tasks); diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index 6eefe4aee5..b796951aa0 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -42,6 +42,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.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.systemui.shared.recents.model.Task; @@ -301,6 +302,8 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, /** * Registers a listener for running tasks + * TODO(b/343292503): Should we remove RunningTasksListener entirely if it's not needed? + * (Note that Desktop mode gets the running tasks by checking {@link DesktopTask#tasks} */ public void registerRunningTasksListener(RunningTasksListener listener) { mTaskList.registerRunningTasksListener(listener); @@ -313,6 +316,20 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, mTaskList.unregisterRunningTasksListener(); } + /** + * Registers a listener for recent tasks + */ + public void registerRecentTasksChangedListener(RecentTasksChangedListener listener) { + mTaskList.registerRecentTasksChangedListener(listener); + } + + /** + * Removes the previously registered running tasks listener + */ + public void unregisterRecentTasksChangedListener() { + mTaskList.unregisterRecentTasksChangedListener(); + } + /** * Gets the set of running tasks. */ @@ -379,4 +396,14 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, */ void onRunningTasksChanged(); } + + /** + * Listener for receiving recent tasks changes + */ + public interface RecentTasksChangedListener { + /** + * Called when there's a change to recent tasks + */ + void onRecentTasksChanged(); + } } diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt index 104263af5b..d0985610c4 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt @@ -16,24 +16,34 @@ package com.android.launcher3.taskbar -import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.ComponentName import android.content.Intent import android.os.Process import android.os.UserHandle import android.testing.AndroidTestingRunner +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION import com.android.launcher3.model.data.AppInfo import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.statehandlers.DesktopVisibilityController import com.android.quickstep.RecentsModel +import com.android.quickstep.RecentsModel.RecentTasksChangedListener +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.systemui.shared.recents.model.Task import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) @@ -45,173 +55,220 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController private var nextTaskId: Int = 500 + private var taskListChangeId: Int = 1 private lateinit var recentAppsController: TaskbarRecentAppsController + private lateinit var recentTasksChangedListener: RecentTasksChangedListener private lateinit var userHandle: UserHandle @Before fun setUp() { super.setup() userHandle = Process.myUserHandle() + recentAppsController = TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController } recentAppsController.init(taskbarControllers) - recentAppsController.isEnabled = true - recentAppsController.setApps( - ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray() - ) + recentAppsController.canShowRunningApps = true + + val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java) + verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture()) + recentTasksChangedListener = listenerCaptor.value } @Test - fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() { - setInDesktopMode(false) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - - assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())) - .isEqualTo(hotseatItems.toTypedArray()) - } - - @Test - fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() { - setInDesktopMode(false) - val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) - val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - + fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() { + recentAppsController.canShowRunningApps = false + setInDesktopMode(true) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1) val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTaskPackages = emptyList(), + recentTaskPackages = emptyList() + ) assertThat(newHotseatItems.map { it?.targetPackage }) .containsExactlyElementsIn(hotseatPackages) } @Test - fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() { + fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() { + recentAppsController.canShowRunningApps = true setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - - assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())) - .isEqualTo(hotseatItems.toTypedArray()) - } - - @Test - fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() { - setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - val expectedPackages = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTaskPackages = emptyList(), + recentTaskPackages = emptyList() ) + val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) assertThat(newHotseatItems.map { it?.targetPackage }) .containsExactlyElementsIn(expectedPackages) } @Test - fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() { + fun onRecentTasksChanged_cantShowRunning_inDesktopMode_shownTasks_returnsEmptyList() { + recentAppsController.canShowRunningApps = false setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - val runningTasks = - createDesktopTasksFromPackageNames( - listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) - ) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - val expectedPackages = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - ) - assertThat(newHotseatItems.map { it?.targetPackage }) - .containsExactlyElementsIn(expectedPackages) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + recentTaskPackages = emptyList() + ) + assertThat(recentAppsController.shownTasks).isEmpty() } @Test - fun getRunningApps_notInDesktopMode_returnsEmptySet() { + fun onRecentTasksChanged_inDesktopMode_noRunningApps_shownTasks_returnsEmptyList() { + setInDesktopMode(true) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTaskPackages = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + ) + assertThat(recentAppsController.shownTasks).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() { + setInDesktopMode(true) + val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTaskPackages = runningTaskPackages, + recentTaskPackages = emptyList() + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_runningAppIsHotseatItem_shownTasks_returnsDistinctItems() { + setInDesktopMode(true) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTaskPackages = + listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2), + recentTaskPackages = emptyList() + ) + val expectedPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() { setInDesktopMode(false) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - assertThat(recentAppsController.runningApps).isEmpty() - assertThat(recentAppsController.minimizedApps).isEmpty() + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2), + recentTaskPackages = emptyList() + ) + assertThat(recentAppsController.runningAppPackages).isEmpty() + assertThat(recentAppsController.minimizedAppPackages).isEmpty() } @Test - fun getRunningApps_inDesktopMode_returnsRunningApps() { + fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() { setInDesktopMode(true) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() + val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTaskPackages = runningTaskPackages, + recentTaskPackages = emptyList() + ) + assertThat(recentAppsController.runningAppPackages) + .containsExactlyElementsIn(runningTaskPackages) + assertThat(recentAppsController.minimizedAppPackages).isEmpty() + } - assertThat(recentAppsController.runningApps) - .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) - assertThat(recentAppsController.minimizedApps).isEmpty() + @Test + fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() { + setInDesktopMode(true) + val runningTaskPackages = + listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTaskPackages = runningTaskPackages, + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + ) + assertThat(recentAppsController.runningAppPackages) + .containsExactlyElementsIn(runningTaskPackages) + assertThat(recentAppsController.minimizedAppPackages).isEmpty() } @Test fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() { setInDesktopMode(true) - val runningTasks = - ArrayList( - listOf( - createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true }, - createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true }, - createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false }, - ) - ) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() + val runningTaskPackages = + listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) + val minimizedTaskIndices = setOf(2) // RUNNING_APP_PACKAGE_3 + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTaskPackages = runningTaskPackages, + minimizedTaskIndices = minimizedTaskIndices, + recentTaskPackages = emptyList() + ) + assertThat(recentAppsController.runningAppPackages) + .containsExactlyElementsIn(runningTaskPackages) + assertThat(recentAppsController.minimizedAppPackages).containsExactly(RUNNING_APP_PACKAGE_3) + } - assertThat(recentAppsController.runningApps) - .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) - assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3) + @Test + fun getMinimizedApps_inDesktopMode_twoTasksSamePackageOneMinimizedReturnsNotMinimized() { + setInDesktopMode(true) + val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_1) + val minimizedTaskIndices = setOf(1) // The second RUNNING_APP_PACKAGE_1 task. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTaskPackages = runningTaskPackages, + minimizedTaskIndices = minimizedTaskIndices, + recentTaskPackages = emptyList() + ) + assertThat(recentAppsController.runningAppPackages) + .containsExactlyElementsIn(runningTaskPackages.toSet()) + assertThat(recentAppsController.minimizedAppPackages).isEmpty() + } + + private fun prepareHotseatAndRunningAndRecentApps( + hotseatPackages: List, + runningTaskPackages: List, + minimizedTaskIndices: Set = emptySet(), + recentTaskPackages: List, + ): Array { + val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages) + val newHotseatItems = + recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) + val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices) + val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages) + val allTasks = + ArrayList().apply { + if (runningTasks != null) { + add(runningTasks) + } + addAll(recentTasks) + } + doAnswer { + val callback: Consumer> = it.getArgument(0) + callback.accept(allTasks) + taskListChangeId + } + .whenever(mockRecentsModel) + .getTasks(any>>()) + recentTasksChangedListener.onRecentTasksChanged() + return newHotseatItems } private fun createHotseatItemsFromPackageNames(packageNames: List): List { - return packageNames.map { createTestAppInfo(packageName = it) } - } - - private fun createDesktopTasksFromPackageNames( - packageNames: List - ): ArrayList { - return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) }) - } - - private fun createDesktopTaskInfo( - packageName: String, - init: RunningTaskInfo.() -> Unit = { isVisible = true }, - ): RunningTaskInfo { - return RunningTaskInfo().apply { - taskId = nextTaskId++ - configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - realActivity = ComponentName(packageName, "TestActivity") - init() + return packageNames.map { + createTestAppInfo(packageName = it).apply { + container = + if (it.startsWith("predicted")) { + CONTAINER_HOTSEAT_PREDICTION + } else { + CONTAINER_HOTSEAT + } + } } } @@ -220,23 +277,54 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { className: String = "testClassName" ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent()) + private fun createDesktopTask( + packageNames: List, + minimizedTaskIndices: Set + ): DesktopTask? { + if (packageNames.isEmpty()) return null + + return DesktopTask( + ArrayList( + packageNames.mapIndexed { index, packageName -> + createTask(packageName, index !in minimizedTaskIndices) + } + ) + ) + } + + private fun createRecentTasksFromPackageNames(packageNames: List): List { + return packageNames.map { GroupTask(createTask(it)) } + } + + private fun createTask(packageName: String, isVisible: Boolean = true): Task { + return Task( + Task.TaskKey( + nextTaskId++, + WINDOWING_MODE_FREEFORM, + Intent().apply { `package` = packageName }, + ComponentName(packageName, "TestActivity"), + userHandle.identifier, + 0 + ) + ) + .apply { this.isVisible = isVisible } + } + private fun setInDesktopMode(inDesktopMode: Boolean) { whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode) } + private val GroupTask.packageNames: List + get() = tasks.map { task -> task.key.packageName } + private companion object { const val HOTSEAT_PACKAGE_1 = "hotseat1" const val HOTSEAT_PACKAGE_2 = "hotseat2" + const val PREDICTED_PACKAGE_1 = "predicted1" const val RUNNING_APP_PACKAGE_1 = "running1" const val RUNNING_APP_PACKAGE_2 = "running2" const val RUNNING_APP_PACKAGE_3 = "running3" - val ALL_APP_PACKAGES = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - RUNNING_APP_PACKAGE_3, - ) + const val RECENT_PACKAGE_1 = "recent1" + const val RECENT_PACKAGE_2 = "recent2" } }