Load and draw Recent/Running tasks that aren't in Hotseat
- Re-support GroupTask as a tag on TaskbarView BubbleTextViews
- Can tap to launch (startActivityFromRecents) or drag and drop
- Move divider when enable_recents_in_taskbar is true
Behavior:
- While in Desktop mode, all open tasks show up in TaskbarView,
either in the Hotseat section or the Running apps section
past the divider. Each one also has a running line indicator.
- While in Fullscreen mode, up to 2 Recent apps that are not
part of Hotseat will be added to the right of the divider.
Flag: com.android.launcher3.enable_recents_in_taskbar
Test: TaskbarRecentAppsControllerTest
Bug: 315354060
Change-Id: I2e2870cca434b51f276a701860edb32069109159
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 4f5922c..efe42fb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -82,6 +82,7 @@
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.views.BubbleTextHolder;
import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LogUtils;
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.systemui.shared.recents.model.Task;
@@ -181,7 +182,9 @@
private DragView startInternalDrag(
BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) {
- float iconScale = btv.getIcon().getAnimatedScale();
+ // TODO(b/344038728): null check is only necessary because Recents doesn't use
+ // FastBitmapDrawable
+ float iconScale = btv.getIcon() == null ? 1f : btv.getIcon().getAnimatedScale();
// Clear the pressed state if necessary
btv.clearFocus();
@@ -248,7 +251,7 @@
dragLayerX + dragOffset.x,
dragLayerY + dragOffset.y,
(View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */,
- (ItemInfo) btv.getTag(),
+ btv.getTag() instanceof ItemInfo itemInfo ? itemInfo : null,
dragRect,
scale * iconScale,
scale,
@@ -288,7 +291,9 @@
initialDragViewScale,
dragViewScaleOnDrop,
scalePx);
- dragView.setItemInfo(dragInfo);
+ if (dragInfo != null) {
+ dragView.setItemInfo(dragInfo);
+ }
mDragObject.dragComplete = false;
mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
@@ -301,7 +306,8 @@
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
- mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
+ mDragObject.originalDragInfo =
+ mDragObject.dragInfo != null ? mDragObject.dragInfo.makeShallowCopy() : null;
if (mOptions.preDragCondition != null) {
dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0
@@ -431,8 +437,8 @@
null, item.user));
}
intent.putExtra(Intent.EXTRA_USER, item.user);
- } else if (tag instanceof Task) {
- Task task = (Task) tag;
+ } else if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+ Task task = groupTask.task1;
clipDescription = new ClipDescription(task.titleDescription,
new String[] {
ClipDescription.MIMETYPE_APPLICATION_TASK
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 365c5c4..0b7ae39 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -33,6 +33,7 @@
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.GroupTask;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -130,7 +131,7 @@
final int itemCount = mContainer.getChildCount();
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
View item = mContainer.getChildAt(itemIdx);
- if (op.evaluate((ItemInfo) item.getTag(), item)) {
+ if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
return;
}
}
@@ -201,18 +202,20 @@
if (mDeferUpdatesForSUW) {
ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
mDeferredUpdates = () ->
- commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages,
+ commitHotseatItemUpdates(finalHotseatItemInfos,
+ recentAppsController.getShownTasks(), runningPackages,
minimizedPackages);
} else {
- commitHotseatItemUpdates(hotseatItemInfos, runningPackages, minimizedPackages);
+ commitHotseatItemUpdates(hotseatItemInfos,
+ recentAppsController.getShownTasks(), runningPackages, minimizedPackages);
}
}
- private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, Set<String> runningPackages,
- Set<String> minimizedPackages) {
- mContainer.updateHotseatItems(hotseatItemInfos);
- mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages,
- minimizedPackages);
+ private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
+ Set<String> runningPackages, Set<String> minimizedPackages) {
+ mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
+ mControllers.taskbarViewController.updateIconViewsRunningStates(
+ runningPackages, minimizedPackages);
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 2730be1..b697590 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -148,8 +148,8 @@
icon.clearFocus();
return null;
}
- ItemInfo item = (ItemInfo) icon.getTag();
- if (!ShortcutUtil.supportsShortcuts(item)) {
+ // TODO(b/344657629) support GroupTask as well, for Taskbar Recent apps
+ if (!(icon.getTag() instanceof ItemInfo item) || !ShortcutUtil.supportsShortcuts(item)) {
return null;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 659f7c8..fc3b4c7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -16,15 +16,19 @@
package com.android.launcher3.taskbar
import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags.enableRecentsInTaskbar
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
+import com.android.launcher3.util.CancellableTask
import com.android.quickstep.RecentsModel
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
import com.android.window.flags.Flags.enableDesktopWindowingMode
import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
import java.io.PrintWriter
+import java.util.function.Consumer
/**
* Provides recent apps functionality, when the Taskbar Recent Apps section is enabled. Behavior:
@@ -46,13 +50,19 @@
field = isEnabledFromTest
}
+ // TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
+ var canShowRecentApps = enableRecentsInTaskbar()
+ @VisibleForTesting
+ set(isEnabledFromTest) {
+ field = isEnabledFromTest
+ }
+
// Initialized in init.
private lateinit var controllers: TaskbarControllers
private var shownHotseatItems: List<ItemInfo> = emptyList()
private var allRecentTasks: List<GroupTask> = emptyList()
private var desktopTask: DesktopTask? = null
- // TODO(next CL): actually read and show these
var shownTasks: List<GroupTask> = emptyList()
private set
@@ -93,6 +103,8 @@
private val recentTasksChangedListener =
RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() }
+ private val iconLoadRequests: MutableSet<CancellableTask<*>> = HashSet()
+
// 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
@@ -107,12 +119,15 @@
fun onDestroy() {
recentsModel.unregisterRecentTasksChangedListener()
+ iconLoadRequests.forEach { it.cancel() }
+ iconLoadRequests.clear()
}
/** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */
fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo?>): Array<ItemInfo?> {
// Ignore predicted apps - we show running or recent apps instead.
- val removePredictions = isInDesktopMode && canShowRunningApps
+ val removePredictions =
+ (isInDesktopMode && canShowRunningApps) || (!isInDesktopMode && canShowRecentApps)
if (!removePredictions) {
shownHotseatItems = hotseatItems.filterNotNull()
onRecentsOrHotseatChanged()
@@ -148,6 +163,17 @@
} else {
computeShownRecentTasks()
}
+
+ for (groupTask in shownTasks) {
+ for (task in groupTask.tasks) {
+ val callback =
+ Consumer<Task> { controllers.taskbarViewController.onTaskUpdated(it) }
+ val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback)
+ if (cancellableTask != null) {
+ iconLoadRequests.add(cancellableTask)
+ }
+ }
+ }
}
private fun computeShownRunningTasks(): List<GroupTask> {
@@ -169,8 +195,18 @@
}
private fun computeShownRecentTasks(): List<GroupTask> {
- // TODO(next CL): implement Recents section
- return emptyList()
+ if (!canShowRecentApps || allRecentTasks.isEmpty()) {
+ return emptyList()
+ }
+ // Remove the current task.
+ val allRecentTasks = allRecentTasks.subList(0, allRecentTasks.size - 1)
+ // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too
+ var shownTasks = dedupeHotseatTasks(allRecentTasks, shownHotseatItems)
+ if (shownTasks.size > MAX_RECENT_TASKS) {
+ // Remove any tasks older than MAX_RECENT_TASKS.
+ shownTasks = shownTasks.subList(shownTasks.size - MAX_RECENT_TASKS, shownTasks.size)
+ }
+ return shownTasks
}
private fun dedupeHotseatTasks(
@@ -187,6 +223,7 @@
override fun dumpLogs(prefix: String, pw: PrintWriter) {
pw.println("$prefix TaskbarRecentAppsController:")
pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
+ pw.println("$prefix\tcanShowRecentApps=$canShowRecentApps")
pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}")
pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}")
pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}")
@@ -197,4 +234,8 @@
private val GroupTask.packageNames: List<String>
get() = tasks.map { task -> task.key.packageName }
+
+ private companion object {
+ const val MAX_RECENT_TASKS = 2
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 570221c..c42d6c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.enableRecentsInTaskbar;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR;
@@ -30,6 +31,7 @@
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.DisplayCutout;
@@ -67,7 +69,11 @@
import com.android.launcher3.views.IconButtonView;
import com.android.quickstep.DeviceConfigWrapper;
import com.android.quickstep.util.AssistStateManager;
+import com.android.quickstep.util.DesktopTask;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
+import java.util.List;
import java.util.function.Predicate;
/**
@@ -168,7 +174,7 @@
mAllAppsButton.setForegroundTint(
mActivityContext.getColor(R.color.all_apps_button_color));
- if (enableTaskbarPinning()) {
+ if (enableTaskbarPinning() || enableRecentsInTaskbar()) {
mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
R.layout.taskbar_divider,
this, false);
@@ -308,9 +314,10 @@
/**
* Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
*/
- protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+ protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
int nextViewIndex = 0;
int numViewsAnimated = 0;
+ boolean addedDividerForRecents = false;
if (mAllAppsButton != null) {
removeView(mAllAppsButton);
@@ -321,8 +328,8 @@
}
removeView(mQsb);
- for (int i = 0; i < hotseatItemInfos.length; i++) {
- ItemInfo hotseatItemInfo = hotseatItemInfos[i];
+ // Add Hotseat icons.
+ for (ItemInfo hotseatItemInfo : hotseatItemInfos) {
if (hotseatItemInfo == null) {
continue;
}
@@ -388,11 +395,8 @@
}
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
- if (hotseatView instanceof BubbleTextView
- && hotseatItemInfo instanceof WorkspaceItemInfo) {
- BubbleTextView btv = (BubbleTextView) hotseatView;
- WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo;
-
+ if (hotseatView instanceof BubbleTextView btv
+ && hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) {
boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
if (animate) {
@@ -405,6 +409,67 @@
}
nextViewIndex++;
}
+
+ if (mTaskbarDivider != null && !recentTasks.isEmpty()) {
+ addView(mTaskbarDivider, nextViewIndex++);
+ addedDividerForRecents = true;
+ }
+
+ // Add Recent/Running icons.
+ for (GroupTask task : recentTasks) {
+ // Replace any Recent views with the appropriate type if it's not already that type.
+ final int expectedLayoutResId;
+ boolean isCollection = false;
+ if (task.hasMultipleTasks()) {
+ if (task instanceof DesktopTask) {
+ // TODO(b/316004172): use Desktop tile layout.
+ expectedLayoutResId = -1;
+ } else {
+ // TODO(b/343289567): use R.layout.app_pair_icon
+ expectedLayoutResId = -1;
+ }
+ isCollection = true;
+ } else {
+ expectedLayoutResId = R.layout.taskbar_app_icon;
+ }
+
+ View recentIcon = null;
+ while (nextViewIndex < getChildCount()) {
+ recentIcon = getChildAt(nextViewIndex);
+
+ // see if the view can be reused
+ if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId)
+ || (isCollection && (recentIcon.getTag() != task))) {
+ removeAndRecycle(recentIcon);
+ recentIcon = null;
+ } else {
+ // View found
+ break;
+ }
+ }
+
+ if (recentIcon == null) {
+ if (isCollection) {
+ // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+ continue;
+ }
+
+ recentIcon = inflate(expectedLayoutResId);
+ LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
+ recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+ addView(recentIcon, nextViewIndex, lp);
+ }
+
+ if (recentIcon instanceof BubbleTextView btv) {
+ applyGroupTaskToBubbleTextView(btv, task);
+ }
+ setClickAndLongClickListenersForIcon(recentIcon);
+ if (enableCursorHoverStates()) {
+ setHoverListenerForIcon(recentIcon);
+ }
+ nextViewIndex++;
+ }
+
// Remove remaining views
while (nextViewIndex < getChildCount()) {
removeAndRecycle(getChildAt(nextViewIndex));
@@ -413,8 +478,8 @@
if (mAllAppsButton != null) {
addView(mAllAppsButton, mIsRtl ? getChildCount() : 0);
- // if only all apps button present, don't include divider view.
- if (mTaskbarDivider != null && getChildCount() > 1) {
+ // If there are no recent tasks, add divider after All Apps (unless it's the only view).
+ if (!addedDividerForRecents && mTaskbarDivider != null && getChildCount() > 1) {
addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1);
}
}
@@ -425,6 +490,20 @@
}
}
+ /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
+ public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) {
+ // TODO(b/343289567): support app pairs.
+ Task task1 = groupTask.task1;
+ // TODO(b/344038728): use FastBitmapDrawable instead of Drawable, to get disabled state
+ // while dragging.
+ Drawable taskIcon = groupTask.task1.icon;
+ if (taskIcon != null) {
+ taskIcon = taskIcon.getConstantState().newDrawable().mutate();
+ }
+ btv.applyIconAndLabel(taskIcon, task1.titleDescription);
+ btv.setTag(groupTask);
+ }
+
/**
* Sets OnClickListener and OnLongClickListener for the given view.
*/
@@ -677,7 +756,8 @@
// map over all the shortcuts on the taskbar
for (int i = 0; i < getChildCount(); i++) {
View item = getChildAt(i);
- if (op.evaluate((ItemInfo) item.getTag(), item)) {
+ // TODO(b/344657629): Support GroupTask as well for notification dots/popup
+ if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
return;
}
}
@@ -694,6 +774,7 @@
View item = getChildAt(i);
if (!(item.getTag() instanceof ItemInfo)) {
// Should only happen for All Apps button.
+ // Will also happen for Recent/Running app icons. (Which have GroupTask as tags)
continue;
}
ItemInfo info = (ItemInfo) item.getTag();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index b03894de..e59a016 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -70,6 +70,8 @@
import com.android.launcher3.util.MultiTranslateDelegate;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.views.IconButtonView;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
import java.io.PrintWriter;
import java.util.Set;
@@ -519,21 +521,31 @@
for (View iconView : getIconViews()) {
if (iconView instanceof BubbleTextView btv) {
btv.updateRunningState(
- getRunningAppState(btv.getTargetPackageName(), runningPackages,
- minimizedPackages));
+ getRunningAppState(btv, runningPackages, minimizedPackages));
}
}
}
private BubbleTextView.RunningAppState getRunningAppState(
- String packageName,
+ BubbleTextView btv,
Set<String> runningPackages,
Set<String> minimizedPackages) {
- if (minimizedPackages.contains(packageName)) {
- return BubbleTextView.RunningAppState.MINIMIZED;
+ Object tag = btv.getTag();
+ if (tag instanceof ItemInfo itemInfo) {
+ if (minimizedPackages.contains(itemInfo.getTargetPackage())) {
+ return BubbleTextView.RunningAppState.MINIMIZED;
+ }
+ if (runningPackages.contains(itemInfo.getTargetPackage())) {
+ return BubbleTextView.RunningAppState.RUNNING;
+ }
}
- if (runningPackages.contains(packageName)) {
- return BubbleTextView.RunningAppState.RUNNING;
+ if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+ if (minimizedPackages.contains(groupTask.task1.key.getPackageName())) {
+ return BubbleTextView.RunningAppState.MINIMIZED;
+ }
+ if (runningPackages.contains(groupTask.task1.key.getPackageName())) {
+ return BubbleTextView.RunningAppState.RUNNING;
+ }
}
return BubbleTextView.RunningAppState.NOT_RUNNING;
}
@@ -873,6 +885,22 @@
mModelCallbacks.commitRunningAppsToUI();
}
+ /**
+ * To be called when the given Task is updated, so that we can tell TaskbarView to also update.
+ * @param task The Task whose e.g. icon changed.
+ */
+ public void onTaskUpdated(Task task) {
+ // Find the icon view(s) that changed.
+ for (View view : mTaskbarView.getIconViews()) {
+ if (view instanceof BubbleTextView btv
+ && view.getTag() instanceof GroupTask groupTask) {
+ if (groupTask.containsTask(task.key.id)) {
+ mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask);
+ }
+ }
+ }
+ }
+
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarViewController:");
@@ -892,5 +920,4 @@
mModelCallbacks.dumpLogs(prefix + "\t", pw);
}
-
}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 5813708..486dc68 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -29,6 +29,7 @@
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.quickstep.RecentsModel
import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.TaskIconCache
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
@@ -51,6 +52,7 @@
@get:Rule val mockitoRule = MockitoJUnit.rule()
+ @Mock private lateinit var mockIconCache: TaskIconCache
@Mock private lateinit var mockRecentsModel: RecentsModel
@Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
@@ -66,10 +68,12 @@
super.setup()
userHandle = Process.myUserHandle()
+ whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
recentAppsController =
TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
recentAppsController.init(taskbarControllers)
recentAppsController.canShowRunningApps = true
+ recentAppsController.canShowRecentApps = true
val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
@@ -92,6 +96,21 @@
}
@Test
+ fun updateHotseatItemInfos_cantShowRecent_notInDesktopMode_returnsAllHotseatItems() {
+ recentAppsController.canShowRecentApps = false
+ setInDesktopMode(false)
+ val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1)
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = hotseatPackages,
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(newHotseatItems.map { it?.targetPackage })
+ .containsExactlyElementsIn(hotseatPackages)
+ }
+
+ @Test
fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() {
recentAppsController.canShowRunningApps = true
setInDesktopMode(true)
@@ -107,6 +126,21 @@
}
@Test
+ fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() {
+ recentAppsController.canShowRecentApps = true
+ setInDesktopMode(false)
+ val newHotseatItems =
+ 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 onRecentTasksChanged_cantShowRunning_inDesktopMode_shownTasks_returnsEmptyList() {
recentAppsController.canShowRunningApps = false
setInDesktopMode(true)
@@ -119,6 +153,30 @@
}
@Test
+ fun onRecentTasksChanged_cantShowRecent_notInDesktopMode_shownTasks_returnsEmptyList() {
+ recentAppsController.canShowRecentApps = false
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_noRecentTasks_shownTasks_returnsEmptyList() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
fun onRecentTasksChanged_inDesktopMode_noRunningApps_shownTasks_returnsEmptyList() {
setInDesktopMode(true)
prepareHotseatAndRunningAndRecentApps(
@@ -249,6 +307,24 @@
}
@Test
+ fun onRecentTasksChanged_notInDesktopMode_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3))
+ }
+
+ @Test
fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() {
setInDesktopMode(true)
prepareHotseatAndRunningAndRecentApps(
@@ -269,6 +345,24 @@
}
@Test
+ fun onRecentTasksChanged_notInDesktopMode_addTask_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3))
+ }
+
+ @Test
fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() {
setInDesktopMode(true)
prepareHotseatAndRunningAndRecentApps(
@@ -287,6 +381,24 @@
}
@Test
+ fun onRecentTasksChanged_notInDesktopMode_removeTask_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_3).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2))
+ }
+
+ @Test
fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() {
setInDesktopMode(false)
val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
@@ -302,6 +414,70 @@
assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
}
+ @Test
+ fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = recentTaskPackages
+ )
+ setInDesktopMode(false)
+ recentTasksChangedListener.onRecentTasksChanged()
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Don't expect RECENT_PACKAGE_3 because it is currently running.
+ val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentTasks_shownTasks_returnsRecentTasks() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // RECENT_PACKAGE_3 is the top task (visible to user) so should be excluded.
+ val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
+ // Only 2 recent tasks shown: Desktop Tile + 1 Recent Task
+ val desktopTilePackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1)
+ val expectedPackages = listOf(desktopTilePackages, recentTaskPackages)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentAndSplitTasks_shownTasks_returnsRecentTaskAndPair() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
+ // Only 2 recent tasks shown: Pair + 1 Recent Task
+ val pairPackages = RECENT_SPLIT_PACKAGES_1.split("_")
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1)
+ val expectedPackages = listOf(pairPackages, recentTaskPackages)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
private fun prepareHotseatAndRunningAndRecentApps(
hotseatPackages: List<String>,
runningTaskPackages: List<String>,
@@ -365,7 +541,18 @@
}
private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
- return packageNames.map { GroupTask(createTask(it)) }
+ return packageNames.map {
+ if (it.startsWith("split")) {
+ val splitPackages = it.split("_")
+ GroupTask(
+ createTask(splitPackages[0]),
+ createTask(splitPackages[1]),
+ /* splitBounds = */ null
+ )
+ } else {
+ GroupTask(createTask(it))
+ }
+ }
}
private fun createTask(packageName: String, isVisible: Boolean = true): Task {
@@ -398,5 +585,7 @@
const val RUNNING_APP_PACKAGE_3 = "running3"
const val RECENT_PACKAGE_1 = "recent1"
const val RECENT_PACKAGE_2 = "recent2"
+ const val RECENT_PACKAGE_3 = "recent3"
+ const val RECENT_SPLIT_PACKAGES_1 = "split1_split2"
}
}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 7d09164..83427a0 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -430,10 +430,21 @@
setDownloadStateContentDescription(info, info.getProgressLevel());
}
+ /**
+ * Directly set the icon and label.
+ */
+ @UiThread
+ public void applyIconAndLabel(Drawable icon, CharSequence label) {
+ applyCompoundDrawables(icon);
+ setText(label);
+ setContentDescription(label);
+ }
+
/** Updates whether the app this view represents is currently running. */
@UiThread
public void updateRunningState(RunningAppState runningAppState) {
mRunningAppState = runningAppState;
+ invalidate();
}
protected void setItemInfo(ItemInfoWithIcon itemInfo) {
@@ -1291,13 +1302,4 @@
public boolean canShowLongPressPopup() {
return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag());
}
-
- /** Returns the package name of the app this icon represents. */
- public String getTargetPackageName() {
- Object tag = getTag();
- if (tag instanceof ItemInfo itemInfo) {
- return itemInfo.getTargetPackage();
- }
- return null;
- }
}