From c5995c8f6d9ee20bd6e4ea506660feea77f0e929 Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Thu, 23 May 2024 22:30:46 +0000 Subject: [PATCH] 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 --- .../taskbar/TaskbarDragController.java | 18 +- .../taskbar/TaskbarModelCallbacks.java | 19 +- .../taskbar/TaskbarPopupController.java | 4 +- .../taskbar/TaskbarRecentAppsController.kt | 49 ++++- .../launcher3/taskbar/TaskbarView.java | 105 ++++++++-- .../taskbar/TaskbarViewController.java | 43 +++- .../TaskbarRecentAppsControllerTest.kt | 191 +++++++++++++++++- src/com/android/launcher3/BubbleTextView.java | 20 +- 8 files changed, 399 insertions(+), 50 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java index 4f5922c29f..efe42fbf3b 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.IntSet; 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 @@ public class TaskbarDragController extends DragController im 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 @@ public class TaskbarDragController extends DragController im 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 @@ public class TaskbarDragController extends DragController im 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 @@ public class TaskbarDragController extends DragController im 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 @@ public class TaskbarDragController extends DragController im 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 365c5c49e0..0b7ae39e41 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.ItemInfoMatcher; 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 @@ public class TaskbarModelCallbacks implements 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 @@ public class TaskbarModelCallbacks implements 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 runningPackages, - Set minimizedPackages) { - mContainer.updateHotseatItems(hotseatItemInfos); - mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages, - minimizedPackages); + private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List recentTasks, + Set runningPackages, Set 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 2730be1b57..b69759039a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -148,8 +148,8 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba 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 659f7c8948..fc3b4c758e 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 @@ class TaskbarRecentAppsController( 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 = 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 @@ -93,6 +103,8 @@ class TaskbarRecentAppsController( private val recentTasksChangedListener = RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() } + private val iconLoadRequests: MutableSet> = 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 @@ class TaskbarRecentAppsController( 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): Array { // 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 @@ class TaskbarRecentAppsController( } else { computeShownRecentTasks() } + + for (groupTask in shownTasks) { + for (task in groupTask.tasks) { + val callback = + Consumer { controllers.taskbarViewController.onTaskUpdated(it) } + val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback) + if (cancellableTask != null) { + iconLoadRequests.add(cancellableTask) + } + } + } } private fun computeShownRunningTasks(): List { @@ -169,8 +195,18 @@ class TaskbarRecentAppsController( } private fun computeShownRecentTasks(): List { - // 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 @@ class TaskbarRecentAppsController( 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 @@ class TaskbarRecentAppsController( private val GroupTask.packageNames: List 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 570221c872..c42d6c6c1c 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 android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_ 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.Context; 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.ActivityContext; 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar /** * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos. */ - protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) { + protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List recentTasks) { int nextViewIndex = 0; int numViewsAnimated = 0; + boolean addedDividerForRecents = false; if (mAllAppsButton != null) { removeView(mAllAppsButton); @@ -321,8 +328,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } // 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } + /** 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar 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 b03894de25..e59a016e72 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.MultiPropertyFactory; 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 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar 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 runningPackages, Set 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 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar 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 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar 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 5813708df2..486dc688d8 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.model.data.ItemInfo 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 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { @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 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { 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()) @@ -91,6 +95,21 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { .containsExactlyElementsIn(hotseatPackages) } + @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 @@ -106,6 +125,21 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { .containsExactlyElementsIn(expectedPackages) } + @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 @@ -118,6 +152,30 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { assertThat(recentAppsController.shownTasks).isEmpty() } + @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) @@ -248,6 +306,24 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { assertThat(shownPackages).isEqualTo(originalOrder) } + @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) @@ -268,6 +344,24 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { assertThat(shownPackages).isEqualTo(expectedOrder) } + @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) @@ -286,6 +380,24 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { assertThat(shownPackages).isEqualTo(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) } + @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) @@ -302,6 +414,70 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { 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, runningTaskPackages: List, @@ -365,7 +541,18 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { } private fun createRecentTasksFromPackageNames(packageNames: List): List { - 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 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { 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 7d09164feb..83427a01ed 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -430,10 +430,21 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, 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 class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, 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; - } }