Merge "Disable Select mode from Menu in fake landscape" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index d6ee92f..73819b3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -15,12 +15,7 @@
*/
package com.android.launcher3.taskbar;
-import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
import android.animation.Animator;
-import android.app.ActivityOptions;
import android.view.KeyEvent;
import android.view.animation.AnimationUtils;
import android.window.RemoteTransition;
@@ -31,13 +26,10 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.SlideInRemoteTransition;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import java.io.PrintWriter;
@@ -158,28 +150,8 @@
AnimationUtils.loadInterpolator(
context, android.R.interpolator.fast_out_extra_slow_in)),
"SlideInTransition");
- if (task instanceof DesktopTask) {
- UI_HELPER_EXECUTOR.execute(() ->
- SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
- .showDesktopApps(
- mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
- remoteTransition));
- } else if (mOnDesktop) {
- UI_HELPER_EXECUTOR.execute(() ->
- SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
- .showDesktopApp(task.task1.key.id));
- } else if (task.task2 == null) {
- UI_HELPER_EXECUTOR.execute(() -> {
- ActivityOptions activityOptions = mControllers.taskbarActivityContext
- .makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
- activityOptions.setRemoteTransition(remoteTransition);
-
- ActivityManagerWrapper.getInstance().startActivityFromRecents(
- task.task1.key, activityOptions);
- });
- } else {
- mControllers.uiController.launchSplitTasks(task, remoteTransition);
- }
+ mControllers.taskbarActivityContext.handleGroupTaskLaunch(
+ task, remoteTransition, mOnDesktop);
return -1;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 6b62c86..5020206 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -67,6 +67,7 @@
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Toast;
+import android.window.RemoteTransition;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -131,6 +132,9 @@
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.NavHandle;
import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.DesktopTask;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
@@ -298,7 +302,7 @@
TaskbarEduTooltipController.newInstance(this),
new KeyboardQuickSwitchController(),
new TaskbarPinningController(this, () ->
- DisplayController.INSTANCE.get(this).getInfo().isInDesktopMode()),
+ DisplayController.isInDesktopMode(this)),
bubbleControllersOptional);
mLauncherPrefs = LauncherPrefs.get(this);
@@ -1081,10 +1085,9 @@
RecentsView recents = taskbarUIController.getRecentsView();
boolean shouldCloseAllOpenViews = true;
Object tag = view.getTag();
- if (tag instanceof Task) {
- Task task = (Task) tag;
- ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
- ActivityOptions.makeBasic());
+ if (tag instanceof GroupTask groupTask) {
+ handleGroupTaskLaunch(groupTask, /* remoteTransition = */ null,
+ DisplayController.isInDesktopMode(this));
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
} else if (tag instanceof FolderInfo) {
// Tapping an expandable folder icon on Taskbar
@@ -1185,6 +1188,36 @@
}
/**
+ * Launches the given GroupTask with the following behavior:
+ * - If the GroupTask is a DesktopTask, launch the tasks in that Desktop.
+ * - If {@code onDesktop}, bring the given GroupTask to the front.
+ * - If the GroupTask is a single task, launch it via startActivityFromRecents.
+ * - Otherwise, we assume the GroupTask is a Split pair and launch them together.
+ */
+ public void handleGroupTaskLaunch(GroupTask task, @Nullable RemoteTransition remoteTransition,
+ boolean onDesktop) {
+ if (task instanceof DesktopTask) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
+ remoteTransition));
+ } else if (onDesktop) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id));
+ } else if (task.task2 == null) {
+ UI_HELPER_EXECUTOR.execute(() -> {
+ ActivityOptions activityOptions =
+ makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
+ activityOptions.setRemoteTransition(remoteTransition);
+
+ ActivityManagerWrapper.getInstance().startActivityFromRecents(
+ task.task1.key, activityOptions);
+ });
+ } else {
+ mControllers.uiController.launchSplitTasks(task, remoteTransition);
+ }
+ }
+
+ /**
* Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app),
* and calls the appropriate method to animate and launch.
*/
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 2b0e169..0b7ae39 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.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,7 @@
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 com.android.quickstep.util.GroupTask;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -54,7 +49,7 @@
* Launcher model Callbacks for rendering taskbar.
*/
public class TaskbarModelCallbacks implements
- BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener {
+ BgDataModel.Callbacks, LauncherBindableItemsContainer {
private final SparseArray<ItemInfo> mHotseatItems = new SparseArray<>();
private List<ItemInfo> mPredictedItems = Collections.emptyList();
@@ -68,8 +63,6 @@
// 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 +72,6 @@
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
@@ -171,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;
}
}
@@ -232,26 +192,30 @@
predictionNextIndex++;
}
}
- hotseatItemInfos = mControllers.taskbarRecentAppsController
- .updateHotseatItemInfos(hotseatItemInfos);
- Set<String> runningPackages = mControllers.taskbarRecentAppsController.getRunningApps();
- Set<String> minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps();
+
+ final TaskbarRecentAppsController recentAppsController =
+ mControllers.taskbarRecentAppsController;
+ hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
+ Set<String> runningPackages = recentAppsController.getRunningAppPackages();
+ Set<String> minimizedPackages = recentAppsController.getMinimizedAppPackages();
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);
}
/**
@@ -270,21 +234,11 @@
}
}
- @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<ComponentKey, Integer> deepShortcutMapCopy) {
mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy);
@@ -296,7 +250,6 @@
Map<PackageUserKey, Integer> 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/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 b1fc9cc..fc3b4c7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,19 +15,20 @@
*/
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.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:
@@ -42,22 +43,28 @@
) : LoggableTaskbarController {
// TODO(b/335401172): unify DesktopMode checks in Launcher.
- val canShowRunningApps =
+ var canShowRunningApps =
enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
+ @VisibleForTesting
+ set(isEnabledFromTest) {
+ field = isEnabledFromTest
+ }
// TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
- var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps
+ var canShowRecentApps = enableRecentsInTaskbar()
@VisibleForTesting
- set(isEnabledFromTest){
+ set(isEnabledFromTest) {
field = isEnabledFromTest
}
// Initialized in init.
private lateinit var controllers: TaskbarControllers
- private var apps: Array<AppInfo>? = null
- private var allRunningDesktopAppInfos: List<AppInfo>? = null
- private var allMinimizedDesktopAppInfos: List<AppInfo>? = null
+ private var shownHotseatItems: List<ItemInfo> = emptyList()
+ private var allRecentTasks: List<GroupTask> = emptyList()
+ private var desktopTask: DesktopTask? = null
+ var shownTasks: List<GroupTask> = emptyList()
+ private set
private val desktopVisibilityController: DesktopVisibilityController?
get() = desktopVisibilityControllerProvider()
@@ -65,122 +72,170 @@
private val isInDesktopMode: Boolean
get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
- val runningApps: Set<String>
+ val runningAppPackages: Set<String>
+ /**
+ * 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<String>
+ val minimizedAppPackages: Set<String>
+ /**
+ * 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() }
+
+ 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
+ // 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<AppInfo>?) {
- this.apps = apps
+ recentsModel.unregisterRecentTasksChangedListener()
+ iconLoadRequests.forEach { it.cancel() }
+ iconLoadRequests.clear()
}
/** 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<ItemInfo?>): Array<ItemInfo?> {
- if (!isEnabled || !isInDesktopMode) {
+ // Ignore predicted apps - we show running or recent apps instead.
+ val removePredictions =
+ (isInDesktopMode && canShowRunningApps) || (!isInDesktopMode && canShowRecentApps)
+ 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<DesktopTask>().firstOrNull()
+ onRecentsOrHotseatChanged()
+ controllers.taskbarViewController.commitRunningAppsToUI()
+ }
+ }
+ }
+
+ private fun onRecentsOrHotseatChanged() {
+ shownTasks =
+ if (isInDesktopMode) {
+ computeShownRunningTasks()
+ } else {
+ computeShownRecentTasks()
}
- if (runningDesktopAppInfos != null) {
- newHotseatItemInfos.addAll(runningDesktopAppInfos)
+
+ 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)
+ }
+ }
}
- return newHotseatItemInfos.toTypedArray()
}
- private fun getRunningDesktopAppInfosExceptHotseatApps(
- allRunningDesktopAppInfos: List<AppInfo>,
- hotseatItems: List<ItemInfo>
- ): List<ItemInfo> {
- val hotseatPackages = hotseatItems.map { it.targetPackage }
- return allRunningDesktopAppInfos
- .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) }
- .map { WorkspaceItemInfo(it) }
- }
-
- private fun getDesktopRunningTasks(): List<RunningTaskInfo> =
- 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<RunningTaskInfo>): List<AppInfo> {
- // Early return if apps is empty, since we then have no AppInfo to compare to
- if (apps == null) {
+ private fun computeShownRunningTasks(): List<GroupTask> {
+ 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.
+ var desktopTaskAsList = tasks.map { GroupTask(it) }
+ // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too.
+ desktopTaskAsList = dedupeHotseatTasks(desktopTaskAsList, shownHotseatItems)
+ val desktopPackages = desktopTaskAsList.map { it.packageNames }
+ // Remove any missing Tasks.
+ val newShownTasks = shownTasks.filter { it.packageNames in desktopPackages }.toMutableList()
+ val newShownPackages = newShownTasks.map { it.packageNames }
+ // Add any new Tasks, maintaining the order from previous shownTasks.
+ newShownTasks.addAll(desktopTaskAsList.filter { it.packageNames !in newShownPackages })
+ return newShownTasks.toList()
}
- /** 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<GroupTask> {
+ if (!canShowRecentApps || allRecentTasks.isEmpty()) {
+ return emptyList()
}
- val runningTasks = getDesktopRunningTasks()
- val runningAppInfo = getAppInfosFromRunningTasks(runningTasks)
- allRunningDesktopAppInfos = runningAppInfo
- updateMinimizedApps(runningTasks, runningAppInfo)
- controllers.taskbarViewController.commitRunningAppsToUI()
+ // 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 updateMinimizedApps(
- runningTasks: List<RunningTaskInfo>,
- runningAppInfo: List<AppInfo>,
- ) {
- 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()
+ private fun dedupeHotseatTasks(
+ groupTasks: List<GroupTask>,
+ shownHotseatItems: List<ItemInfo>
+ ): List<GroupTask> {
+ val hotseatPackages = shownHotseatItems.map { item -> item.targetPackage }
+ return groupTasks.filter { groupTask ->
+ groupTask.hasMultipleTasks() ||
+ !hotseatPackages.contains(groupTask.task1.key.packageName)
+ }
}
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\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}")
+ pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}")
+ pw.println("$prefix\trunningTasks=$runningAppPackages")
+ pw.println("$prefix\tminimizedTasks=$minimizedAppPackages")
+ }
+
+ 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/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 7ff887c..6279903 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -245,7 +245,7 @@
private Animator mTaskbarBackgroundAlphaAnimator;
private long mTaskbarBackgroundDuration;
- private boolean mIsGoingHome;
+ private boolean mUserIsNotGoingHome = false;
// Evaluate whether the handle should be stashed
private final LongPredicate mIsStashedPredicate = flags -> {
@@ -828,17 +828,13 @@
private boolean mTaskbarBgAlphaAnimationStarted = false;
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
- if (mIsGoingHome) {
- mTaskbarBgAlphaAnimationStarted = true;
- }
if (mTaskbarBgAlphaAnimationStarted) {
return;
}
if (valueAnimator.getAnimatedFraction() >= ANIMATED_FRACTION_THRESHOLD) {
- if (!mIsGoingHome) {
+ if (mUserIsNotGoingHome) {
playTaskbarBackgroundAlphaAnimation();
- setUserIsGoingHome(false);
mTaskbarBgAlphaAnimationStarted = true;
}
}
@@ -850,8 +846,8 @@
/**
* Sets whether the user is going home based on the current gesture.
*/
- public void setUserIsGoingHome(boolean isGoingHome) {
- mIsGoingHome = isGoingHome;
+ public void setUserIsNotGoingHome(boolean userIsNotGoingHome) {
+ mUserIsNotGoingHome = userIsNotGoingHome;
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 593285f..ce281c3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -415,7 +415,7 @@
/**
* Sets whether the user is going home based on the current gesture.
*/
- public void setUserIsGoingHome(boolean isGoingHome) {
- mControllers.taskbarStashController.setUserIsGoingHome(isGoingHome);
+ public void setUserIsNotGoingHome(boolean isNotGoingHome) {
+ mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome);
}
}
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 55745b5..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;
@@ -224,7 +226,6 @@
}
LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks);
mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
- mModelCallbacks.unregisterListeners();
}
public boolean areIconsVisible() {
@@ -520,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;
}
@@ -869,6 +880,27 @@
return mTaskbarView.isEventOverAnyItem(ev);
}
+ /** Called when there's a change in running apps to update the UI. */
+ public void commitRunningAppsToUI() {
+ 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:");
@@ -888,15 +920,4 @@
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/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 028df34..15e4578 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -600,7 +600,7 @@
Bitmap bitmap = createOverflowBitmap(context);
LayoutInflater inflater = LayoutInflater.from(context);
BubbleView bubbleView = (BubbleView) inflater.inflate(
- R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */);
+ R.layout.bubble_bar_overflow_button, mBarView, false /* attachToRoot */);
BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
bubbleView.setOverflow(overflow, bitmap);
return overflow;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 43e21f4..39d1ed7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -34,4 +34,8 @@
) : BubbleBarItem(info.key, view)
/** Represents the overflow bubble in the bubble bar. */
-data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
+data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem(KEY, view) {
+ companion object {
+ const val KEY = "Overflow"
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 010e5dc..0ea5031 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -44,6 +44,7 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
import com.android.launcher3.util.DisplayController;
import com.android.wm.shell.Flags;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
@@ -101,8 +102,6 @@
// During fade in animation we shift the bubble bar 1/60th of the screen width
private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f;
- private static final int SCALE_IN_ANIMATION_DURATION_MS = 250;
-
/**
* Custom property to set alpha value for the bar view while a bubble is being dragged.
* Skips applying alpha to the dragged bubble.
@@ -161,11 +160,12 @@
// collapsed state and 1 to the fully expanded state.
private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
- /** An animator used for scaling in a new bubble to the bubble bar while expanded. */
+ /** An animator used for animating individual bubbles in the bubble bar while expanded. */
@Nullable
- private ValueAnimator mNewBubbleScaleInAnimator = null;
+ private BubbleAnimator mBubbleAnimator = null;
@Nullable
private ValueAnimator mScalePaddingAnimator;
+
@Nullable
private Animator mBubbleBarLocationAnimator = null;
@@ -671,38 +671,37 @@
bubble.setScaleX(0f);
bubble.setScaleY(0f);
addView(bubble, 0, lp);
- createNewBubbleScaleInAnimator(bubble);
- mNewBubbleScaleInAnimator.start();
+
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ updateWidth();
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ bubble.setScaleX(1);
+ bubble.setScaleY(1);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ bubble.setScaleX(animatedFraction);
+ bubble.setScaleY(animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
} else {
addView(bubble, 0, lp);
}
}
- private void createNewBubbleScaleInAnimator(View bubble) {
- mNewBubbleScaleInAnimator = ValueAnimator.ofFloat(0, 1);
- mNewBubbleScaleInAnimator.setDuration(SCALE_IN_ANIMATION_DURATION_MS);
- mNewBubbleScaleInAnimator.addUpdateListener(animation -> {
- float animatedFraction = animation.getAnimatedFraction();
- bubble.setScaleX(animatedFraction);
- bubble.setScaleY(animatedFraction);
- updateBubblesLayoutProperties(mBubbleBarLocation);
- invalidate();
- });
- mNewBubbleScaleInAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(Animator animation) {
- bubble.setScaleX(1);
- bubble.setScaleY(1);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- updateWidth();
- mNewBubbleScaleInAnimator = null;
- }
- });
- }
-
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -717,6 +716,50 @@
updateContentDescription();
}
+ /** Removes the given bubble from the bubble bar. */
+ public void removeBubble(View bubble) {
+ if (isExpanded()) {
+ // TODO b/347062801 - animate the bubble bar if the last bubble is removed
+ int bubbleCount = getChildCount();
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ removeView(bubble);
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ bubble.setScaleX(0);
+ bubble.setScaleY(0);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ bubble.setScaleX(1 - animatedFraction);
+ bubble.setScaleY(1 - animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ int bubbleIndex = indexOfChild(bubble);
+ BubbleView lastBubble = (BubbleView) getChildAt(bubbleCount - 1);
+ String lastBubbleKey = lastBubble.getBubble().getKey();
+ boolean removingLastBubble =
+ BubbleBarOverflow.KEY.equals(lastBubbleKey)
+ ? bubbleIndex == bubbleCount - 2
+ : bubbleIndex == bubbleCount - 1;
+ mBubbleAnimator.animateRemovedBubble(
+ indexOfChild(bubble), indexOfChild(mSelectedBubbleView), removingLastBubble,
+ listener);
+ } else {
+ removeView(bubble);
+ }
+ }
+
// TODO: (b/283309949) animate it
@Override
public void removeView(View view) {
@@ -782,9 +825,14 @@
bv.setDragTranslationX(0f);
bv.setOffsetX(0f);
- bv.setScaleX(mIconScale);
- bv.setScaleY(mIconScale);
+ if (mBubbleAnimator == null || !mBubbleAnimator.isRunning()) {
+ // if the bubble animator is running don't set scale here, it will be set by the
+ // animator
+ bv.setScaleX(mIconScale);
+ bv.setScaleY(mIconScale);
+ }
bv.setTranslationY(ty);
+
// the position of the bubble when the bar is fully expanded
final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
// the position of the bubble when the bar is fully collapsed
@@ -862,9 +910,8 @@
}
final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
float translationX;
- if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) {
- translationX = getExpandedBubbleTranslationXDuringScaleAnimation(
- bubbleIndex, bubbleCount, onLeft);
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getExpandedBubbleTranslationX(bubbleIndex) + mBubbleBarPadding;
} else if (onLeft) {
translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
} else {
@@ -873,51 +920,6 @@
return translationX - getScaleIconShift();
}
- /**
- * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
- * expanded <b>and</b> a new bubble is animating in.
- *
- * <p>This method assumes that the animation is running so callers are expected to verify that
- * before calling it.
- */
- private float getExpandedBubbleTranslationXDuringScaleAnimation(
- int bubbleIndex, int bubbleCount, boolean onLeft) {
- // when the new bubble scale animation is running, a new bubble is animating in while the
- // bubble bar is expanded, so we have at least 2 bubbles in the bubble bar - the expanded
- // one, and the new one animating in.
-
- if (mNewBubbleScaleInAnimator == null) {
- // callers of this method are expected to verify that the animation is running, but the
- // compiler doesn't know that.
- return 0;
- }
- final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
- final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction();
- // the new bubble is scaling in from the center, so we need to adjust its translation so
- // that the distance to the adjacent bubble scales at the same rate.
- final float pivotAdjustment = -(1 - newBubbleScale) * getScaledIconSize() / 2f;
-
- if (onLeft) {
- if (bubbleIndex == 0) {
- // this is the animating bubble. use scaled spacing between it and the bubble to
- // its left
- return (bubbleCount - 1) * getScaledIconSize()
- + (bubbleCount - 2) * mExpandedBarIconsSpacing
- + newBubbleScale * mExpandedBarIconsSpacing
- + pivotAdjustment;
- }
- // when the bubble bar is on the left, only the translation of the right-most bubble
- // is affected by the scale animation.
- return (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
- } else if (bubbleIndex == 0) {
- // the bubble bar is on the right, and this is the animating bubble. it only needs
- // to be adjusted for the scaling pivot.
- return pivotAdjustment;
- } else {
- return iconAndSpacing * (bubbleIndex - 1 + newBubbleScale);
- }
- }
-
private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount,
boolean onLeft) {
if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
@@ -980,9 +982,11 @@
BubbleView previouslySelectedBubble = mSelectedBubbleView;
mSelectedBubbleView = view;
mBubbleBarBackground.showArrow(view != null);
- // TODO: (b/283309949) remove animation should be implemented first, so than arrow
- // animation is adjusted, skip animation for now
- updateArrowForSelected(previouslySelectedBubble != null);
+
+ // if bubbles are being animated, the arrow position will be set as part of the animation
+ if (mBubbleAnimator == null) {
+ updateArrowForSelected(previouslySelectedBubble != null);
+ }
}
/**
@@ -1037,6 +1041,9 @@
}
private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) {
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getArrowPosition() + mBubbleBarPadding;
+ }
final int index = indexOfChild(mSelectedBubbleView);
final float selectedBubbleTranslationX = getExpandedBubbleTranslationX(
index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -1102,20 +1109,14 @@
*/
public float expandedWidth() {
final int childCount = getChildCount();
- // spaces amount is less than child count by 1, or 0 if no child views
- final float totalSpace;
- final float totalIconSize;
- if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) {
- // when this animation is running, a new bubble is animating in while the bubble bar is
- // expanded, so we have at least 2 bubbles in the bubble bar.
- final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction();
- totalSpace = (childCount - 2 + newBubbleScale) * mExpandedBarIconsSpacing;
- totalIconSize = (childCount - 1 + newBubbleScale) * getScaledIconSize();
- } else {
- totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing;
- totalIconSize = childCount * getScaledIconSize();
+ final float horizontalPadding = 2 * mBubbleBarPadding;
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getExpandedWidth() + horizontalPadding;
}
- return totalIconSize + totalSpace + 2 * mBubbleBarPadding;
+ // spaces amount is less than child count by 1, or 0 if no child views
+ final float totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing;
+ final float totalIconSize = childCount * getScaledIconSize();
+ return totalIconSize + totalSpace + horizontalPadding;
}
private float collapsedWidth() {
@@ -1166,7 +1167,6 @@
return mIsAnimatingNewBubble;
}
-
private boolean hasOverview() {
// Overview is always the last bubble
View lastChild = getChildAt(getChildCount() - 1);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 951b99d..da0826b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -388,7 +388,7 @@
*/
public void removeBubble(BubbleBarItem b) {
if (b != null) {
- mBarView.removeView(b.getView());
+ mBarView.removeBubble(b.getView());
} else {
Log.w(TAG, "removeBubble, bubble was null!");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
new file mode 100644
index 0000000..7672743
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.animation
+
+import androidx.core.animation.Animator
+import androidx.core.animation.ValueAnimator
+
+/**
+ * Animates individual bubbles within the bubble bar while the bubble bar is expanded.
+ *
+ * This class should only be kept for the duration of the animation and a new instance should be
+ * created for each animation.
+ */
+class BubbleAnimator(
+ private val iconSize: Float,
+ private val expandedBarIconSpacing: Float,
+ private val bubbleCount: Int,
+ private val onLeft: Boolean,
+) {
+
+ companion object {
+ const val ANIMATION_DURATION_MS = 250L
+ }
+
+ private var state: State = State.Idle
+ private lateinit var animator: ValueAnimator
+
+ fun animateNewBubble(selectedBubbleIndex: Int, listener: Listener) {
+ animator = createAnimator(listener)
+ state = State.AddingBubble(selectedBubbleIndex)
+ animator.start()
+ }
+
+ fun animateRemovedBubble(
+ bubbleIndex: Int,
+ selectedBubbleIndex: Int,
+ removingLastBubble: Boolean,
+ listener: Listener
+ ) {
+ animator = createAnimator(listener)
+ state = State.RemovingBubble(bubbleIndex, selectedBubbleIndex, removingLastBubble)
+ animator.start()
+ }
+
+ private fun createAnimator(listener: Listener): ValueAnimator {
+ val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
+ animator.addUpdateListener { animation ->
+ val animatedFraction = (animation as ValueAnimator).animatedFraction
+ listener.onAnimationUpdate(animatedFraction)
+ }
+ animator.addListener(
+ object : Animator.AnimatorListener {
+
+ override fun onAnimationCancel(animation: Animator) {
+ listener.onAnimationCancel()
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ state = State.Idle
+ listener.onAnimationEnd()
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {}
+
+ override fun onAnimationStart(animation: Animator) {}
+ }
+ )
+ return animator
+ }
+
+ /**
+ * The translation X of the bubble at index [bubbleIndex] according to the progress of the
+ * animation.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getExpandedBubbleTranslationX(bubbleIndex: Int): Float {
+ return when (val state = state) {
+ State.Idle -> 0f
+ is State.AddingBubble ->
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = bubbleIndex,
+ scalingBubbleIndex = 0,
+ bubbleScale = animator.animatedFraction
+ )
+ is State.RemovingBubble ->
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = bubbleIndex,
+ scalingBubbleIndex = state.bubbleIndex,
+ bubbleScale = 1 - animator.animatedFraction
+ )
+ }
+ }
+
+ /**
+ * The expanded width of the bubble bar according to the progress of the animation.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getExpandedWidth(): Float {
+ val bubbleScale =
+ when (state) {
+ State.Idle -> 0f
+ is State.AddingBubble -> animator.animatedFraction
+ is State.RemovingBubble -> 1 - animator.animatedFraction
+ }
+ // When this animator is running the bubble bar is expanded so it's safe to assume that we
+ // have at least 2 bubbles, but should update the logic to support optional overflow.
+ // If we're removing the last bubble, the entire bar should animate and we shouldn't get
+ // here.
+ val totalSpace = (bubbleCount - 2 + bubbleScale) * expandedBarIconSpacing
+ val totalIconSize = (bubbleCount - 1 + bubbleScale) * iconSize
+ return totalIconSize + totalSpace
+ }
+
+ /**
+ * Returns the arrow position according to the progress of the animation and, if the selected
+ * bubble is being removed, accounting to the newly selected bubble.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getArrowPosition(): Float {
+ return when (val state = state) {
+ State.Idle -> 0f
+ is State.AddingBubble -> {
+ val tx =
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = state.selectedBubbleIndex,
+ scalingBubbleIndex = 0,
+ bubbleScale = animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ }
+ is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
+ }
+ }
+
+ private fun getArrowPositionWhenRemovingBubble(state: State.RemovingBubble): Float {
+ return if (state.selectedBubbleIndex != state.bubbleIndex) {
+ // if we're not removing the selected bubble, the selected bubble doesn't change so just
+ // return the translation X of the selected bubble and add half icon
+ val tx =
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = state.selectedBubbleIndex,
+ scalingBubbleIndex = state.bubbleIndex,
+ bubbleScale = 1 - animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ } else {
+ // we're removing the selected bubble so the arrow needs to point to a different bubble.
+ // if we're removing the last bubble the newly selected bubble will be the second to
+ // last. otherwise, it'll be the next bubble (closer to the overflow)
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ if (state.removingLastBubble) {
+ if (onLeft) {
+ // the newly selected bubble is the bubble to the right. at the end of the
+ // animation all the bubbles will have shifted left, so the arrow stays at the
+ // same distance from the left edge of bar
+ (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f
+ } else {
+ // the newly selected bubble is the bubble to the left. at the end of the
+ // animation all the bubbles will have shifted right, and the arrow would
+ // eventually be closer to the left edge of the bar by iconAndSpacing
+ val initialTx = state.bubbleIndex * iconAndSpacing + iconSize / 2f
+ initialTx - animator.animatedFraction * iconAndSpacing
+ }
+ } else {
+ if (onLeft) {
+ // the newly selected bubble is to the left, and bubbles are shifting left, so
+ // move the arrow closer to the left edge of the bar by iconAndSpacing
+ val initialTx =
+ (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f
+ initialTx - animator.animatedFraction * iconAndSpacing
+ } else {
+ // the newly selected bubble is to the right, and bubbles are shifting right, so
+ // the arrow stays at the same distance from the left edge of the bar
+ state.bubbleIndex * iconAndSpacing + iconSize / 2f
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
+ * expanded and a bubble is animating in or out.
+ *
+ * @param bubbleIndex the index of the bubble for which the translation is requested
+ * @param scalingBubbleIndex the index of the bubble that is animating
+ * @param bubbleScale the current scale of the animating bubble
+ */
+ private fun getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex: Int,
+ scalingBubbleIndex: Int,
+ bubbleScale: Float
+ ): Float {
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ // the bubble is scaling from the center, so we need to adjust its translation so
+ // that the distance to the adjacent bubble scales at the same rate.
+ val pivotAdjustment = -(1 - bubbleScale) * iconSize / 2f
+
+ return if (onLeft) {
+ when {
+ bubbleIndex < scalingBubbleIndex ->
+ // the bar is on the left and the current bubble is to the right of the scaling
+ // bubble so account for its scale
+ (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing
+ bubbleIndex == scalingBubbleIndex -> {
+ // the bar is on the left and this is the scaling bubble
+ val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize
+ // don't count the spacing between the scaling bubble and the bubble on the left
+ // because we need to scale that space
+ val totalSpacing = (bubbleCount - bubbleIndex - 2) * expandedBarIconSpacing
+ val scaledSpace = bubbleScale * expandedBarIconSpacing
+ totalIconSize + totalSpacing + scaledSpace + pivotAdjustment
+ }
+ else ->
+ // the bar is on the left and the scaling bubble is on the right. the current
+ // bubble is unaffected by the scaling bubble
+ (bubbleCount - bubbleIndex - 1) * iconAndSpacing
+ }
+ } else {
+ when {
+ bubbleIndex < scalingBubbleIndex ->
+ // the bar is on the right and the scaling bubble is on the right. the current
+ // bubble is unaffected by the scaling bubble
+ iconAndSpacing * bubbleIndex
+ bubbleIndex == scalingBubbleIndex ->
+ // the bar is on the right, and this is the animating bubble. it only needs to
+ // be adjusted for the scaling pivot.
+ iconAndSpacing * bubbleIndex + pivotAdjustment
+ else ->
+ // the bar is on the right and the scaling bubble is on the left so account for
+ // its scale
+ iconAndSpacing * (bubbleIndex - 1 + bubbleScale)
+ }
+ }
+ }
+
+ val isRunning: Boolean
+ get() = state != State.Idle
+
+ /** The state of the animation. */
+ sealed interface State {
+
+ /** The animation is not running. */
+ data object Idle : State
+
+ /** A new bubble is being added to the bubble bar. */
+ data class AddingBubble(val selectedBubbleIndex: Int) : State
+
+ /** A bubble is being removed from the bubble bar. */
+ data class RemovingBubble(
+ /** The index of the bubble being removed. */
+ val bubbleIndex: Int,
+ /** The index of the selected bubble. */
+ val selectedBubbleIndex: Int,
+ /** Whether the bubble being removed is also the last bubble. */
+ val removingLastBubble: Boolean
+ ) : State
+ }
+
+ /** Callbacks for the animation. */
+ interface Listener {
+
+ /**
+ * Notifies the listener of an animation update event, where `animatedFraction` represents
+ * the progress of the animation starting from 0 and ending at 1.
+ */
+ fun onAnimationUpdate(animatedFraction: Float)
+
+ /** Notifies the listener that the animation was canceled. */
+ fun onAnimationCancel()
+
+ /** Notifies that listener that the animation ended. */
+ fun onAnimationEnd()
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 2dcd932..feff9fd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -41,7 +41,7 @@
private var animatingBubble: AnimatingBubble? = null
private val bubbleBarBounceDistanceInPx =
- bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
+ bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
private companion object {
/** The time to show the flyout. */
@@ -347,7 +347,7 @@
*/
private fun buildBubbleBarBounceAnimation() = Runnable {
bubbleBarView.onAnimatingBubbleStarted()
- val ty = bubbleBarView.translationY
+ val ty = bubbleStashController.bubbleBarTranslationY
val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
springBackAnimation.setDefaultSpringConfig(springConfig)
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
index 668a87d..ac7dd06 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
@@ -33,7 +33,7 @@
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/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 1acafab..93f72fc 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1196,7 +1196,7 @@
}
if (mContainerInterface.getTaskbarController() != null) {
// Resets this value as the gesture is now complete.
- mContainerInterface.getTaskbarController().setUserIsGoingHome(false);
+ mContainerInterface.getTaskbarController().setUserIsNotGoingHome(false);
}
ActiveGestureLog.INSTANCE.addLog(
new ActiveGestureLog.CompoundString("onSettledOnEndTarget ")
@@ -1350,7 +1350,7 @@
&& mIsTransientTaskbar
&& mContainerInterface.getTaskbarController() != null) {
mContainerInterface.getTaskbarController()
- .setUserIsGoingHome(endTarget == GestureState.GestureEndTarget.HOME);
+ .setUserIsNotGoingHome(endTarget != GestureState.GestureEndTarget.HOME);
}
float endShift = endTarget == ALL_APPS ? mDragLengthFactor
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 7da92bc..8f533a3 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -265,6 +265,10 @@
case TYPE_HOME:
ActiveGestureLog.INSTANCE.addLog(
"OverviewCommandHelper.executeCommand(TYPE_HOME)");
+ // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
+ // we should still call it on main thread because launcher is waiting for
+ // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
+ // could potentially delay resuming launcher. See b/348668521 for more details.
mService.startActivity(mOverviewComponentObserver.getHomeIntent());
return true;
case TYPE_SHOW:
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index b08a46f..66091d4 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -70,7 +70,8 @@
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<ActivityManager.RunningTaskInfo> mRunningTasks;
@@ -199,6 +200,9 @@
public void onRecentTasksChanged() {
invalidateLoadedTasks();
+ if (mRecentTasksChangedListener != null) {
+ mRecentTasksChangedListener.onRecentTasksChanged();
+ }
}
private synchronized void invalidateLoadedTasks() {
@@ -221,6 +225,21 @@
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<ActivityManager.RunningTaskInfo> runningTasks) {
// Tasks are retrieved in order of most recently launched/used to least recently launched.
mRunningTasks = new ArrayList<>(runningTasks);
@@ -357,6 +376,7 @@
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 6eefe4a..b796951 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.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 @@
/**
* 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);
@@ -314,6 +317,20 @@
}
/**
+ * 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.
*/
public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks() {
@@ -379,4 +396,14 @@
*/
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/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index d18c86e..4691ea9 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -228,6 +228,7 @@
// Take the thumbnail of the task without a scrim and apply it back after
float alpha = mThumbnailView.getDimAlpha();
+ // TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval
mThumbnailView.setDimAlpha(0);
Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 2836c89..dbe2b19 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -53,25 +53,31 @@
TaskThumbnailViewModel(
recentsView.mRecentsViewData,
(parent as TaskView).taskViewData,
+ (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
recentsView.mTasksRepository,
)
}
private var uiState: TaskThumbnailUiState = Uninitialized
private var inheritedScale: Float = 1f
+ private var dimProgress: Float = 0f
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val scrimPaint = Paint().apply { color = Color.BLACK }
private val _measuredBounds = Rect()
private val measuredBounds: Rect
get() {
_measuredBounds.set(0, 0, measuredWidth, measuredHeight)
return _measuredBounds
}
+
private var cornerRadius: Float = TaskCornerRadius.get(context)
private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
constructor(context: Context?) : super(context)
+
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+
constructor(
context: Context?,
attrs: AttributeSet?,
@@ -87,6 +93,13 @@
invalidate()
}
}
+ MainScope().launch {
+ viewModel.dimProgress.collect { dimProgress ->
+ // TODO(b/348195366) Add fade in/out for scrim
+ this@TaskThumbnailView.dimProgress = dimProgress
+ invalidate()
+ }
+ }
MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } }
MainScope().launch {
viewModel.inheritedScale.collect { viewModelInheritedScale ->
@@ -111,6 +124,10 @@
is Snapshot -> drawSnapshotState(canvas, uiStateVal)
is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor)
}
+
+ if (dimProgress > 0) {
+ drawScrim(canvas)
+ }
}
private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) {
@@ -135,6 +152,11 @@
canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null)
}
+ private fun drawScrim(canvas: Canvas) {
+ scrimPaint.alpha = (dimProgress * MAX_SCRIM_ALPHA).toInt()
+ canvas.drawRect(measuredBounds, scrimPaint)
+ }
+
private fun getCurrentCornerRadius() =
Utilities.mapRange(
viewModel.recentsFullscreenProgress.value,
@@ -145,5 +167,6 @@
companion object {
private val CLEAR_PAINT =
Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
+ private const val MAX_SCRIM_ALPHA = (0.4f * 255).toInt()
}
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
index 4511ea7..fe21174 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
@@ -25,6 +25,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,6 +41,7 @@
class TaskThumbnailViewModel(
recentsViewData: RecentsViewData,
taskViewData: TaskViewData,
+ taskContainerData: TaskContainerData,
private val tasksRepository: RecentTasksRepository,
) {
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
@@ -50,6 +52,7 @@
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
recentsScale * taskScale
}
+ val dimProgress: Flow<Float> = taskContainerData.taskMenuOpenProgress
val uiState: Flow<TaskThumbnailUiState> =
task
.flatMapLatest { taskFlow ->
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
new file mode 100644
index 0000000..769424c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class TaskContainerData {
+ val taskMenuOpenProgress = MutableStateFlow(0f)
+}
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index e44f148..2b944bc 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -164,22 +164,7 @@
}
if (sourceRectHint.isEmpty()) {
- // Crop a Rect matches the aspect ratio and pivots at the center point.
- // To make the animation path simplified.
- if ((appBounds.width() / (float) appBounds.height()) > aspectRatio) {
- // use the full height.
- mSourceRectHint.set(0, 0,
- (int) (appBounds.height() * aspectRatio), appBounds.height());
- mSourceRectHint.offset(
- (appBounds.width() - mSourceRectHint.width()) / 2, 0);
- } else {
- // use the full width.
- mSourceRectHint.set(0, 0,
- appBounds.width(), (int) (appBounds.width() / aspectRatio));
- mSourceRectHint.offset(
- 0, (appBounds.height() - mSourceRectHint.height()) / 2);
- }
-
+ mSourceRectHint.set(getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio));
// Create a new overlay layer. We do not call detach on this instance, it's propagated
// to other classes like PipTaskOrganizer / RecentsAnimationController to complete
// the cleanup.
@@ -225,6 +210,26 @@
addOnUpdateListener(this::onAnimationUpdate);
}
+ /**
+ * Crop a Rect matches the aspect ratio and pivots at the center point.
+ */
+ private Rect getEnterPipWithOverlaySrcRectHint(Rect appBounds, float aspectRatio) {
+ final float appBoundsAspectRatio = appBounds.width() / (float) appBounds.height();
+ final int width, height;
+ int left = appBounds.left;
+ int top = appBounds.top;
+ if (appBoundsAspectRatio < aspectRatio) {
+ width = appBounds.width();
+ height = (int) (width / aspectRatio);
+ top = appBounds.top + (appBounds.height() - height) / 2;
+ } else {
+ height = appBounds.height();
+ width = (int) (height * aspectRatio);
+ left = appBounds.left + (appBounds.width() - width) / 2;
+ }
+ return new Rect(left, top, left + width, top + height);
+ }
+
private void onAnimationUpdate(RectF currentRect, float progress) {
if (mHasAnimationEnded) return;
final SurfaceControl.Transaction tx =
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 936f6a1..4c78e21 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -193,23 +193,23 @@
}
val taskContainer =
TaskContainer(
- task,
- // TODO(b/338360089): Support new TTV for DesktopTaskView
- thumbnailView = null,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
- digitalWellBeingToast = null,
- showWindowsView = null,
- taskOverlayFactory
- )
- .apply { thumbnailViewDeprecated.bind(task, overlay) }
+ task,
+ // TODO(b/338360089): Support new TTV for DesktopTaskView
+ thumbnailView = null,
+ thumbnailViewDeprecated,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+ digitalWellBeingToast = null,
+ showWindowsView = null,
+ taskOverlayFactory
+ )
if (index >= taskContainers.size) {
taskContainers.add(taskContainer)
} else {
taskContainers[index] = taskContainer
}
+ taskContainer.bind()
}
repeat(taskContainers.size - tasks.size) {
with(taskContainers.removeLast()) {
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index d6a3376..6296b0e 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -145,6 +145,8 @@
taskOverlayFactory
)
)
+ taskContainers.forEach { it.bind() }
+
this.splitBoundsConfig =
splitBoundsConfig?.also {
taskContainers[0]
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index b332652..d806e3d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -31,6 +31,7 @@
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.app.animation.Interpolators.OVERSHOOT_0_75;
import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
@@ -131,6 +132,7 @@
import androidx.core.graphics.ColorUtils;
import com.android.internal.jank.Cuj;
+import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
@@ -314,7 +316,6 @@
/**
* Can be used to tint the color of the RecentsView to simulate a scrim that can views
* excluded from. Really should be a proper scrim.
- * TODO(b/187528071): Remove this and replace with a real scrim.
*/
private static final FloatProperty<RecentsView> COLOR_TINT =
new FloatProperty<RecentsView>("colorTint") {
@@ -555,7 +556,6 @@
@Nullable
protected GestureState.GestureEndTarget mCurrentGestureEndTarget;
- // TODO(b/187528071): Remove these and replace with a real scrim.
private float mColorTint;
private final int mTintingColor;
@Nullable
@@ -2689,6 +2689,7 @@
}
private void animateRotation(int newRotation) {
+ AbstractFloatingView.closeAllOpenViewsExcept(mContainer, false, TYPE_REBIND_SAFE);
AnimatorSet pa = setRecentsChangedOrientation(true);
pa.addListener(AnimatorListeners.forSuccessCallback(() -> {
setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
@@ -6029,6 +6030,7 @@
* tasks to be dimmed while other elements in the recents view are left alone.
*/
public void showForegroundScrim(boolean show) {
+ // TODO(b/335606129) Add scrim response into new TTV - this is called from overlay
if (!show && mColorTint == 0) {
if (mTintingAnimator != null) {
mTintingAnimator.cancel();
@@ -6044,7 +6046,6 @@
}
/** Tint the RecentsView and TaskViews in to simulate a scrim. */
- // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView
private void setColorTint(float tintAmount) {
mColorTint = tintAmount;
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index eda58c5..4f446b2 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -18,6 +18,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.quickstep.views.TaskThumbnailViewDeprecated.DIM_ALPHA;
@@ -367,6 +368,14 @@
mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA,
closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+ if (enableRefactorTaskThumbnail()) {
+ mRevealAnimator.addUpdateListener(animation -> {
+ float animatedFraction = animation.getAnimatedFraction();
+ float openProgress = closing ? (1 - animatedFraction) : animatedFraction;
+ mTaskContainer.getTaskContainerData()
+ .getTaskMenuOpenProgress().setValue(openProgress);
+ });
+ }
mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationStart(Animator animation) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 9c1aaa6..7a3b00f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -88,6 +88,7 @@
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
import com.android.quickstep.task.thumbnail.TaskThumbnail
import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
@@ -667,6 +668,7 @@
taskOverlayFactory
)
)
+ taskContainers.forEach { it.bind() }
setOrientationState(orientedState)
}
@@ -693,24 +695,16 @@
}
val iconView = getOrInflateIconView(iconViewId)
return TaskContainer(
- task,
- thumbnailView,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- stagePosition,
- DigitalWellBeingToast(container, this),
- findViewById(showWindowViewId)!!,
- taskOverlayFactory
- )
- .apply {
- if (enableRefactorTaskThumbnail()) {
- thumbnailViewDeprecated.setTaskOverlay(overlay)
- bindThumbnailView()
- } else {
- thumbnailViewDeprecated.bind(task, overlay)
- }
- }
+ task,
+ thumbnailView,
+ thumbnailViewDeprecated,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ stagePosition,
+ DigitalWellBeingToast(container, this),
+ findViewById(showWindowViewId)!!,
+ taskOverlayFactory
+ )
}
protected fun getOrInflateIconView(@IdRes iconViewId: Int): TaskViewIcon {
@@ -1379,7 +1373,6 @@
open fun setColorTint(amount: Float, tintColor: Int) {
taskContainers.forEach {
if (!enableRefactorTaskThumbnail()) {
- // TODO(b/334832108) Add scrim to new TTV
it.thumbnailViewDeprecated.dimAlpha = amount
}
it.iconView.setIconColorTint(tintColor, amount)
@@ -1522,6 +1515,9 @@
resetViewTransforms()
}
+ fun getTaskContainerForTaskThumbnailView(taskThumbnailView: TaskThumbnailView): TaskContainer? =
+ taskContainers.firstOrNull { it.thumbnailView == taskThumbnailView }
+
open fun resetViewTransforms() {
// fullscreenTranslation and accumulatedTranslation should not be reset, as
// resetViewTransforms is called during QuickSwitch scrolling.
@@ -1623,6 +1619,7 @@
taskOverlayFactory: TaskOverlayFactory
) {
val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
+ val taskContainerData = TaskContainerData()
val snapshotView: View
get() = thumbnailView ?: thumbnailViewDeprecated
@@ -1656,6 +1653,15 @@
thumbnailView?.let { taskView.removeView(it) }
}
+ fun bind() {
+ if (enableRefactorTaskThumbnail() && thumbnailView != null) {
+ thumbnailViewDeprecated.setTaskOverlay(overlay)
+ bindThumbnailView()
+ } else {
+ thumbnailViewDeprecated.bind(task, overlay)
+ }
+ }
+
// TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
// so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
fun bindThumbnailView() {
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
similarity index 100%
rename from quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
new file mode 100644
index 0000000..20bd617
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.animation
+
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleAnimatorTest {
+
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
+ private lateinit var bubbleAnimator: BubbleAnimator
+
+ @Test
+ fun animateNewBubble_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateNewBubble(selectedBubbleIndex = 2, listener)
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
+ @Test
+ fun animateRemovedBubble_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateRemovedBubble(
+ bubbleIndex = 2,
+ selectedBubbleIndex = 3,
+ removingLastBubble = false,
+ listener
+ )
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
+ private class TestBubbleAnimatorListener : BubbleAnimator.Listener {
+
+ override fun onAnimationUpdate(animatedFraction: Float) {}
+
+ override fun onAnimationCancel() {}
+
+ override fun onAnimationEnd() {}
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 2ae4e6b..e9c0dd6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -388,7 +388,8 @@
fun animateBubbleBarForCollapsed() {
setUpBubbleBar()
setUpBubbleStashController()
- bubbleBarView.translationY = BAR_TRANSLATION_Y_FOR_HOTSEAT
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index 3b8754c..a394b65 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -28,6 +28,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -43,9 +44,10 @@
class TaskThumbnailViewModelTest {
private val recentsViewData = RecentsViewData()
private val taskViewData = TaskViewData()
+ private val taskContainerData = TaskContainerData()
private val tasksRepository = FakeTasksRepository()
private val systemUnderTest =
- TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository)
+ TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
private val tasks = (0..5).map(::createTaskWithId)
diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
similarity index 100%
rename from quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 104263a..486dc68 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -16,24 +16,35 @@
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.TaskIconCache
+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)
@@ -41,177 +52,471 @@
@get:Rule val mockitoRule = MockitoJUnit.rule()
+ @Mock private lateinit var mockIconCache: TaskIconCache
@Mock private lateinit var mockRecentsModel: RecentsModel
@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()
+
+ whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
recentAppsController =
TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
recentAppsController.init(taskbarControllers)
- recentAppsController.isEnabled = true
- recentAppsController.setApps(
- ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
- )
+ recentAppsController.canShowRunningApps = true
+ recentAppsController.canShowRecentApps = 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() {
- 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,
- )
- assertThat(newHotseatItems.map { it?.targetPackage })
- .containsExactlyElementsIn(expectedPackages)
- }
-
- @Test
- fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() {
- 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)
- }
-
- @Test
- fun getRunningApps_notInDesktopMode_returnsEmptySet() {
+ fun updateHotseatItemInfos_cantShowRecent_notInDesktopMode_returnsAllHotseatItems() {
+ recentAppsController.canShowRecentApps = false
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()
+ 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 getRunningApps_inDesktopMode_returnsRunningApps() {
+ fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() {
+ recentAppsController.canShowRunningApps = true
setInDesktopMode(true)
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
+ 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)
+ }
- assertThat(recentAppsController.runningApps)
- .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
- assertThat(recentAppsController.minimizedApps).isEmpty()
+ @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)
+ 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 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(
+ 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)
+ 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 onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() {
+ setInDesktopMode(true)
+ 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()
+ }
+
+ @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()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ val originalOrder = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = originalOrder,
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ 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)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_3),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ val expectedOrder =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
+ 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)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3),
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ 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)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = recentTaskPackages
+ )
+ setInDesktopMode(true)
+ recentTasksChangedListener.onRecentTasksChanged()
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ 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>,
+ minimizedTaskIndices: Set<Int> = emptySet(),
+ recentTaskPackages: List<String>,
+ ): Array<ItemInfo?> {
+ val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
+ val newHotseatItems =
+ recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+ val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices)
+ val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
+ val allTasks =
+ ArrayList<GroupTask>().apply {
+ if (runningTasks != null) {
+ add(runningTasks)
+ }
+ addAll(recentTasks)
+ }
+ doAnswer {
+ val callback: Consumer<ArrayList<GroupTask>> = it.getArgument(0)
+ callback.accept(allTasks)
+ taskListChangeId
+ }
+ .whenever(mockRecentsModel)
+ .getTasks(any<Consumer<List<GroupTask>>>())
+ recentTasksChangedListener.onRecentTasksChanged()
+ return newHotseatItems
}
private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
- return packageNames.map { createTestAppInfo(packageName = it) }
- }
-
- private fun createDesktopTasksFromPackageNames(
- packageNames: List<String>
- ): ArrayList<RunningTaskInfo> {
- 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 +525,67 @@
className: String = "testClassName"
) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
+ private fun createDesktopTask(
+ packageNames: List<String>,
+ minimizedTaskIndices: Set<Int>
+ ): DesktopTask? {
+ if (packageNames.isEmpty()) return null
+
+ return DesktopTask(
+ ArrayList(
+ packageNames.mapIndexed { index, packageName ->
+ createTask(packageName, index !in minimizedTaskIndices)
+ }
+ )
+ )
+ }
+
+ private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
+ 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 {
+ 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<String>
+ 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"
+ const val RECENT_PACKAGE_3 = "recent3"
+ const val RECENT_SPLIT_PACKAGES_1 = "split1_split2"
}
}
diff --git a/res/layout/bubble_bar_overflow_button.xml b/res/layout/bubble_bar_overflow_button.xml
new file mode 100644
index 0000000..cb54990
--- /dev/null
+++ b/res/layout/bubble_bar_overflow_button.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<com.android.launcher3.taskbar.bubbles.BubbleView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bubble_overflow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
\ No newline at end of file
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;
- }
}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 8546454..f775673 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -97,10 +97,9 @@
if (bubbleBarEnabled) {
float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
if (hasBubbles && Float.compare(adjustedBorderSpace, 0f) != 0) {
- getShortcutsAndWidgets().setTranslationProvider(child -> {
- int index = getShortcutsAndWidgets().indexOfChild(child);
+ getShortcutsAndWidgets().setTranslationProvider(cellX -> {
float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace;
- return dp.iconSizePx + index * borderSpaceDelta;
+ return dp.iconSizePx + cellX * borderSpaceDelta;
});
if (mQsb instanceof HorizontalInsettableView) {
HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb;
@@ -147,10 +146,7 @@
// update the translation provider for future layout passes of hotseat icons.
if (isBubbleBarVisible) {
- icons.setTranslationProvider(child -> {
- int index = icons.indexOfChild(child);
- return dp.iconSizePx + index * borderSpaceDelta;
- });
+ icons.setTranslationProvider(cellX -> dp.iconSizePx + cellX * borderSpaceDelta);
} else {
icons.setTranslationProvider(null);
}
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 7484b64..d2c3c78 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -245,7 +245,7 @@
}
child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
if (mTranslationProvider != null) {
- final float tx = mTranslationProvider.getTranslationX(child);
+ final float tx = mTranslationProvider.getTranslationX(lp.getCellX());
if (child instanceof Reorderable) {
((Reorderable) child).getTranslateDelegate()
.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM)
@@ -330,6 +330,6 @@
/** Provides translation values to apply when laying out child views. */
interface TranslationProvider {
- float getTranslationX(View child);
+ float getTranslationX(int cellX);
}
}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 33e6f91..d0596fa 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.config.FeatureFlags.BooleanFlag.DISABLED;
import static com.android.launcher3.config.FeatureFlags.BooleanFlag.ENABLED;
import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
import android.content.res.Resources;
@@ -143,7 +144,7 @@
DISABLED, "Sends a notification whenever launcher encounters an uncaught exception.");
public static final boolean ENABLE_TASKBAR_NAVBAR_UNIFICATION =
- enableTaskbarNavbarUnification() && !isPhone();
+ enableTaskbarNavbarUnification() && (!isPhone() || enableTaskbarOnPhones());
private static boolean isPhone() {
final boolean isPhone;
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 16fabe2..7f36d6f 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -182,6 +182,11 @@
return INSTANCE.get(context).getInfo().isTransientTaskbar();
}
+ /** Returns whether we are currently in Desktop mode. */
+ public static boolean isInDesktopMode(Context context) {
+ return INSTANCE.get(context).getInfo().isInDesktopMode();
+ }
+
/**
* Handles info change for desktop mode.
*/
diff --git a/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt b/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt
new file mode 100644
index 0000000..c5f9f86
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3
+
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxApplication
+import com.android.launcher3.util.SafeCloseable
+
+/**
+ * Initializes [MainThreadInitializedObject] instances for Robolectric tests.
+ *
+ * Unlike instrumentation tests, Robolectric creates a new application instance for each test, which
+ * could cause the various static objects defined in [MainThreadInitializedObject] to leak. Thus, a
+ * [SandboxApplication] for Robolectric tests can implement this interface to limit the lifecycle of
+ * these objects to a single test.
+ */
+interface RoboObjectInitializer {
+
+ /** Overrides an object with [type] to [value]. */
+ fun <T : SafeCloseable> initializeObject(type: MainThreadInitializedObject<T>, value: T)
+}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java b/tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
similarity index 96%
rename from tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
rename to tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
index 9537e1c..1eb4173 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
@@ -24,7 +24,7 @@
import android.content.Context;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.ActivityContextWrapper;
diff --git a/tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameProviderTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
rename to tests/multivalentTests/src/com/android/launcher3/folder/FolderNameProviderTest.java
diff --git a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/tests/multivalentTests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
rename to tests/multivalentTests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
rename to tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java