Merge "Provide reason for moving task to front." into main
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 87a82f0..2406fb6 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -22,6 +22,7 @@
import android.content.Context
import android.graphics.Rect
import android.os.IBinder
+import android.view.Choreographer
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
@@ -32,6 +33,8 @@
import android.window.TransitionInfo.Change
import androidx.core.animation.addListener
import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.quickstep.RemoteRunnable
import com.android.wm.shell.shared.animation.MinimizeAnimator
@@ -49,8 +52,11 @@
private val context: Context,
private val mainExecutor: Executor,
private val launchType: AppLaunchType,
+ @Cuj.CujType private val cujType: Int,
) : RemoteTransitionStub() {
+ private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+
enum class AppLaunchType(
val boundsAnimationParams: WindowAnimator.BoundsAnimationParams,
val alphaDurationMs: Long,
@@ -127,7 +133,10 @@
duration = launchType.alphaDurationMs
interpolator = Interpolators.LINEAR
addUpdateListener { animation ->
- transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
+ transaction
+ .setAlpha(change.leash, animation.animatedValue as Float)
+ .setFrameTimeline(Choreographer.getInstance().vsyncId)
+ .apply()
}
}
val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
@@ -137,8 +146,14 @@
ScreenDecorationsUtils.getWindowCornerRadius(context),
)
return AnimatorSet().apply {
+ interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
playTogether(boundsAnimator, alphaAnimator)
- addListener(onEnd = { animation -> onAnimFinish(animation) })
+ addListener(
+ onEnd = { animation ->
+ onAnimFinish(animation)
+ interactionJankMonitor.end(cujType)
+ }
+ )
}
}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
index 6e36305..6cf9b9e 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -23,6 +23,7 @@
import android.window.RemoteTransition
import android.window.TransitionFilter
import android.window.TransitionFilter.CONTAINER_ORDER_TOP
+import com.android.internal.jank.Cuj
import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.quickstep.SystemUiProxy
@@ -45,8 +46,13 @@
}
remoteWindowLimitUnminimizeTransition =
RemoteTransition(
- DesktopAppLaunchTransition(context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE),
- "DesktopWindowLimitUnminimize"
+ DesktopAppLaunchTransition(
+ context,
+ MAIN_EXECUTOR,
+ AppLaunchType.UNMINIMIZE,
+ Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+ ),
+ "DesktopWindowLimitUnminimize",
)
systemUiProxy.registerRemoteTransition(
remoteWindowLimitUnminimizeTransition,
diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
index a833ccf..b33fd38 100644
--- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -60,9 +61,9 @@
}
@Override
- public void showAppBubble(Intent intent) {
+ public void showAppBubble(Intent intent, UserHandle user) {
if (intent == null || intent.getPackage() == null) return;
- SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
+ SystemUiProxy.INSTANCE.get(this).showAppBubble(intent, user);
}
/** Callback invoked when a drag is initiated within this context. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 79c3469..8cb43d2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -286,7 +286,12 @@
) {
// This app is being unminimized - use our own transition runner.
remoteTransition = new RemoteTransition(
- new DesktopAppLaunchTransition(context, MAIN_EXECUTOR, UNMINIMIZE),
+ new DesktopAppLaunchTransition(
+ context,
+ MAIN_EXECUTOR,
+ UNMINIMIZE,
+ Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH
+ ),
"DesktopKeyboardQuickSwitchUnminimize");
}
mControllers.taskbarActivityContext.handleGroupTaskLaunch(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 5060df8..a0f3948 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -82,6 +82,7 @@
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
+import com.android.internal.jank.Cuj;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
@@ -914,7 +915,11 @@
ActivityOptions options = ActivityOptions.makeRemoteTransition(
new RemoteTransition(
new DesktopAppLaunchTransition(
- /* context= */ this, getMainExecutor(), launchType),
+ /* context= */ this,
+ getMainExecutor(),
+ launchType,
+ Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON
+ ),
"TaskbarDesktopLaunch"));
return new ActivityOptionsWrapper(options, new RunnableList());
}
@@ -1317,7 +1322,8 @@
if (tag instanceof GroupTask groupTask) {
RemoteTransition remoteTransition =
(areDesktopTasksVisible() && canUnminimizeDesktopTask(groupTask.task1.key.id))
- ? createUnminimizeRemoteTransition() : null;
+ ? createUnminimizeRemoteTransition(
+ Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON) : null;
if (areDesktopTasksVisible() && mControllers.uiController.isInOverviewUi()) {
RunnableList runnableList = recents.launchRunningDesktopTaskView();
// Wrapping it in runnable so we post after DW is ready for the app
@@ -1350,7 +1356,8 @@
}
} else if (tag instanceof TaskItemInfo info && !Flags.enableMultiInstanceMenuTaskbar()) {
RemoteTransition remoteTransition = canUnminimizeDesktopTask(info.getTaskId())
- ? createUnminimizeRemoteTransition() : null;
+ ? createUnminimizeRemoteTransition(Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)
+ : null;
TaskView taskView = null;
if (recents != null) {
@@ -1534,9 +1541,14 @@
);
}
- private RemoteTransition createUnminimizeRemoteTransition() {
+ private RemoteTransition createUnminimizeRemoteTransition(@Cuj.CujType int cujType) {
return new RemoteTransition(
- new DesktopAppLaunchTransition(this, getMainExecutor(), AppLaunchType.UNMINIMIZE),
+ new DesktopAppLaunchTransition(
+ this,
+ getMainExecutor(),
+ AppLaunchType.UNMINIMIZE,
+ cujType
+ ),
"TaskbarDesktopUnminimize");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index 3bff31f..b7000db 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -108,7 +108,7 @@
revealHoverToolTip();
mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true);
}
- return true;
+ return false;
}
private void revealHoverToolTip() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index caac35e..b0cb484 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -36,6 +36,7 @@
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.FloatProperty;
+import android.util.Log;
import android.util.Property;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -110,6 +111,8 @@
private boolean mForceHideRing = false;
private Animator mRingScaleAnim;
+ private int mWidth;
+
private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y =
new FloatProperty<PredictedAppIcon>("slotMachineTranslationY") {
@Override
@@ -300,7 +303,13 @@
}
private int getOutlineOffsetX() {
- return (getMeasuredWidth() - mNormalizedIconSize) / 2;
+ int measuredWidth = getMeasuredWidth();
+ if (mDisplay != DISPLAY_TASKBAR) {
+ Log.d("b/387844520", "getOutlineOffsetX: measured width = " + measuredWidth
+ + ", mNormalizedIconSize = " + mNormalizedIconSize
+ + ", last updated width = " + mWidth);
+ }
+ return (mWidth - mNormalizedIconSize) / 2;
}
private int getOutlineOffsetY() {
@@ -313,7 +322,11 @@
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
+ mWidth = w;
mSlotIconBound.offsetTo((w - getIconSize()) / 2, (h - getIconSize()) / 2);
+ if (mDisplay != DISPLAY_TASKBAR) {
+ Log.d("b/387844520", "calling updateRingPath from onSizeChanged");
+ }
updateRingPath();
}
@@ -325,6 +338,7 @@
private void updateRingPath() {
mRingPath.reset();
+ mTmpMatrix.reset();
mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
mRingPath.addPath(mShapePath, mTmpMatrix);
@@ -339,6 +353,7 @@
mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize);
mRingPath.addPath(mShapePath, mTmpMatrix);
}
+ invalidate();
}
@Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index f86030d..58ebc50 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -84,6 +84,7 @@
import android.os.IRemoteCallback;
import android.os.SystemProperties;
import android.os.Trace;
+import android.os.UserHandle;
import android.util.AttributeSet;
import android.view.Display;
import android.view.HapticFeedbackConstants;
@@ -1431,9 +1432,9 @@
}
@Override
- public void showAppBubble(Intent intent) {
+ public void showAppBubble(Intent intent, UserHandle user) {
if (intent == null || intent.getPackage() == null) return;
- SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
+ SystemUiProxy.INSTANCE.get(this).showAppBubble(intent, user);
}
/** Sets the location of the bubble bar */
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index e0fa77a..e1d4536 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -28,7 +28,6 @@
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_HOME;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_NEW_TASK;
-import android.app.TaskInfo;
import android.content.Intent;
import android.os.SystemClock;
import android.view.MotionEvent;
@@ -47,7 +46,6 @@
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.views.RecentsViewContainer;
import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.wm.shell.shared.GroupedTaskInfo;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -334,13 +332,7 @@
return new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
} else {
if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- if (mRunningTask.getVisibleTasks().isEmpty()) {
- return new int[0];
- }
- GroupedTaskInfo topRunningTask = mRunningTask.getVisibleTasks().getFirst();
- List<TaskInfo> groupedTasks = topRunningTask.getTaskInfoList();
- return groupedTasks.stream().mapToInt(
- groupedTask -> groupedTask.taskId).toArray();
+ return mRunningTask.topGroupedTaskIds();
} else {
int cachedTasksSize = mRunningTask.mAllCachedTasks.size();
int count = Math.min(cachedTasksSize, getMultipleTasks ? 2 : 1);
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 32ccd72..ee4ee38 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.KeyguardManager;
@@ -39,6 +40,7 @@
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.systemui.shared.recents.model.Task;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.recents.IRecentTasksListener;
import com.android.wm.shell.shared.GroupedTaskInfo;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -116,7 +118,8 @@
@Override
public void onTaskMovedToFront(GroupedTaskInfo taskToFront) {
mMainThreadExecutor.execute(() -> {
- topTaskTracker.handleTaskMovedToFront(taskToFront.getTaskInfo1());
+ topTaskTracker.handleTaskMovedToFront(
+ taskToFront.getBaseGroupedTask().getTaskInfo1());
});
}
@@ -340,50 +343,74 @@
int numVisibleTasks = 0;
for (GroupedTaskInfo rawTask : rawTasks) {
- if (rawTask.getType() == TYPE_FREEFORM) {
+ if (rawTask.isBaseType(TYPE_FREEFORM)) {
// TYPE_FREEFORM tasks is only created when desktop mode can be entered,
// leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
- GroupTask desktopTask = createDesktopTask(rawTask);
+ GroupTask desktopTask = createDesktopTask(rawTask.getBaseGroupedTask());
if (desktopTask != null) {
allTasks.add(desktopTask);
}
}
continue;
}
- TaskInfo taskInfo1 = rawTask.getTaskInfo1();
- TaskInfo taskInfo2 = rawTask.getTaskInfo2();
- Task.TaskKey task1Key = new Task.TaskKey(taskInfo1);
- Task task1 = loadKeysOnly
- ? new Task(task1Key)
- : Task.from(task1Key, taskInfo1,
- tmpLockedUsers.get(task1Key.userId) /* isLocked */);
- Task task2 = null;
- if (taskInfo2 != null) {
- // Is split task
- Task.TaskKey task2Key = new Task.TaskKey(taskInfo2);
- task2 = loadKeysOnly
- ? new Task(task2Key)
- : Task.from(task2Key, taskInfo2,
- tmpLockedUsers.get(task2Key.userId) /* isLocked */);
+
+ if (Flags.enableShellTopTaskTracking()) {
+ final TaskInfo taskInfo1 = rawTask.getBaseGroupedTask().getTaskInfo1();
+ final Task.TaskKey task1Key = new Task.TaskKey(taskInfo1);
+ final Task task1 = Task.from(task1Key, taskInfo1,
+ tmpLockedUsers.get(task1Key.userId) /* isLocked */);
+ final Task task2;
+ final SplitConfigurationOptions.SplitBounds launcherSplitBounds;
+
+ if (rawTask.isBaseType(TYPE_SPLIT)) {
+ final TaskInfo taskInfo2 = rawTask.getBaseGroupedTask().getTaskInfo2();
+ final Task.TaskKey task2Key = new Task.TaskKey(taskInfo2);
+ task2 = Task.from(task2Key, taskInfo2,
+ tmpLockedUsers.get(task2Key.userId) /* isLocked */);
+ launcherSplitBounds =
+ convertShellSplitBoundsToLauncher(
+ rawTask.getBaseGroupedTask().getSplitBounds());
+ } else {
+ task2 = null;
+ launcherSplitBounds = null;
+ }
+ allTasks.add(new GroupTask(task1, task2, launcherSplitBounds));
} else {
- // Is fullscreen task
- if (numVisibleTasks > 0) {
- boolean isExcluded = (taskInfo1.baseIntent.getFlags()
- & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
- if (taskInfo1.isTopActivityTransparent && isExcluded) {
- // If there are already visible tasks, then ignore the excluded tasks and
- // don't add them to the returned list
- continue;
+ TaskInfo taskInfo1 = rawTask.getTaskInfo1();
+ TaskInfo taskInfo2 = rawTask.getTaskInfo2();
+ Task.TaskKey task1Key = new Task.TaskKey(taskInfo1);
+ Task task1 = loadKeysOnly
+ ? new Task(task1Key)
+ : Task.from(task1Key, taskInfo1,
+ tmpLockedUsers.get(task1Key.userId) /* isLocked */);
+ Task task2 = null;
+ if (taskInfo2 != null) {
+ // Is split task
+ Task.TaskKey task2Key = new Task.TaskKey(taskInfo2);
+ task2 = loadKeysOnly
+ ? new Task(task2Key)
+ : Task.from(task2Key, taskInfo2,
+ tmpLockedUsers.get(task2Key.userId) /* isLocked */);
+ } else {
+ // Is fullscreen task
+ if (numVisibleTasks > 0) {
+ boolean isExcluded = (taskInfo1.baseIntent.getFlags()
+ & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+ if (taskInfo1.isTopActivityTransparent && isExcluded) {
+ // If there are already visible tasks, then ignore the excluded tasks
+ // and don't add them to the returned list
+ continue;
+ }
}
}
+ if (taskInfo1.isVisible) {
+ numVisibleTasks++;
+ }
+ final SplitConfigurationOptions.SplitBounds launcherSplitBounds =
+ convertShellSplitBoundsToLauncher(rawTask.getSplitBounds());
+ allTasks.add(new GroupTask(task1, task2, launcherSplitBounds));
}
- if (taskInfo1.isVisible) {
- numVisibleTasks++;
- }
- final SplitConfigurationOptions.SplitBounds launcherSplitBounds =
- convertShellSplitBoundsToLauncher(rawTask.getSplitBounds());
- allTasks.add(new GroupTask(task1, task2, launcherSplitBounds));
}
return allTasks;
@@ -409,14 +436,6 @@
return new DesktopTask(tasks);
}
- private ArrayList<GroupTask> copyOf(ArrayList<GroupTask> tasks) {
- ArrayList<GroupTask> newTasks = new ArrayList<>();
- for (int i = 0; i < tasks.size(); i++) {
- newTasks.add(tasks.get(i).copy());
- }
- return newTasks;
- }
-
public void dump(String prefix, PrintWriter writer) {
writer.println(prefix + "RecentTasksList:");
writer.println(prefix + " mChangeId=" + mChangeId);
@@ -439,14 +458,7 @@
}
writer.println(prefix + " rawTasks=[");
for (GroupedTaskInfo task : rawTasks) {
- TaskInfo taskInfo1 = task.getTaskInfo1();
- TaskInfo taskInfo2 = task.getTaskInfo2();
- ComponentName cn1 = taskInfo1.topActivity;
- ComponentName cn2 = taskInfo2 != null ? taskInfo2.topActivity : null;
- writer.println(prefix + " t1: (id=" + taskInfo1.taskId
- + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
- + " t2: (id=" + (taskInfo2 != null ? taskInfo2.taskId : "-1")
- + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" : "no package)"));
+ writer.println(prefix + task);
}
writer.println(prefix + " ]");
}
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index fa4cffd..0459da0 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -664,8 +664,10 @@
*
* @param intent the intent used to create the bubble.
*/
- fun showAppBubble(intent: Intent?) =
- executeWithErrorLog({ "Failed call showAppBubble" }) { bubbles?.showAppBubble(intent) }
+ fun showAppBubble(intent: Intent?, user: UserHandle) =
+ executeWithErrorLog({ "Failed call showAppBubble" }) {
+ bubbles?.showAppBubble(intent, user)
+ }
/** Tells SysUI to show the expanded view. */
fun showExpandedView() =
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index bfd6107..b3d9da3 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -24,12 +24,12 @@
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_A;
+import static com.android.wm.shell.Flags.enableShellTopTaskTracking;
import static com.android.wm.shell.Flags.enableFlexibleSplit;
import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
-import android.content.Context;
import android.util.ArrayMap;
import android.util.Log;
@@ -37,7 +37,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
-import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
@@ -77,8 +76,6 @@
private static final int HISTORY_SIZE = 5;
- private final Context mContext;
-
// Only used when Flags.enableShellTopTaskTracking() is disabled
// Ordered list with first item being the most recent task.
private final LinkedList<TaskInfo> mOrderedTaskList = new LinkedList<>();
@@ -87,20 +84,13 @@
private int mPinnedTaskId = INVALID_TASK_ID;
// Only used when Flags.enableShellTopTaskTracking() is enabled
- // Mapping of display id to running tasks. Running tasks are ordered from top most to
- // bottom most.
- private ArrayMap<Integer, ArrayList<GroupedTaskInfo>> mVisibleTasks = new ArrayMap<>();
+ // Mapping of display id to visible tasks. Visible tasks are ordered from top most to bottom
+ // most.
+ private ArrayMap<Integer, GroupedTaskInfo> mVisibleTasks = new ArrayMap<>();
@Inject
- public TopTaskTracker(@ApplicationContext Context context, DaggerSingletonTracker tracker,
- SystemUiProxy systemUiProxy) {
- mContext = context;
-
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- // Just prepopulate a list for the default display tasks so we don't need to add null
- // checks everywhere
- mVisibleTasks.put(DEFAULT_DISPLAY, new ArrayList<>());
- } else {
+ public TopTaskTracker(DaggerSingletonTracker tracker, SystemUiProxy systemUiProxy) {
+ if (!enableShellTopTaskTracking()) {
mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN;
mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
@@ -109,7 +99,7 @@
}
tracker.addCloseable(() -> {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -120,7 +110,7 @@
@Override
public void onTaskRemoved(int taskId) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -133,7 +123,7 @@
}
void handleTaskMovedToFront(TaskInfo taskInfo) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -187,32 +177,25 @@
* Called when the set of visible tasks have changed.
*/
public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
- if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (!enableShellTopTaskTracking()) {
return;
}
- // TODO(346588978): Per-display info, just have everything in order by display
-
// Clear existing tasks for each display
- mVisibleTasks.forEach((displayId, visibleTasksOnDisplay) -> visibleTasksOnDisplay.clear());
+ mVisibleTasks.clear();
// Update the visible tasks on each display
- for (int i = 0; i < visibleTasks.length; i++) {
- final int displayId = visibleTasks[i].getTaskInfo1().getDisplayId();
- final ArrayList<GroupedTaskInfo> displayTasks;
- if (mVisibleTasks.containsKey(displayId)) {
- displayTasks = mVisibleTasks.get(displayId);
- } else {
- displayTasks = new ArrayList<>();
- mVisibleTasks.put(displayId, displayTasks);
- }
- displayTasks.add(visibleTasks[i]);
+ Log.d(TAG, "onVisibleTasksChanged:");
+ for (GroupedTaskInfo groupedTask : visibleTasks) {
+ Log.d(TAG, "\t" + groupedTask);
+ final int displayId = groupedTask.getBaseGroupedTask().getTaskInfo1().getDisplayId();
+ mVisibleTasks.put(displayId, groupedTask);
}
}
@Override
public void onStagePositionChanged(@StageType int stage, @StagePosition int position) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -224,7 +207,7 @@
}
public void onTaskChanged(RunningTaskInfo taskInfo) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -238,7 +221,7 @@
@Override
public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -262,7 +245,7 @@
@Override
public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -271,7 +254,7 @@
@Override
public void onActivityUnpinned() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
return;
}
@@ -279,16 +262,17 @@
}
/**
- * @return index 0 will be task in left/top position, index 1 in right/bottom position.
- * Will return empty array if device is not in staged split
+ * Return the running split task ids. Index 0 will be task in left/top position, index 1 in
+ * right/bottom position, or and empty array if device is not in splitscreen.
*/
public int[] getRunningSplitTaskIds() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- // TODO(346588978): This assumes default display for now
- final ArrayList<GroupedTaskInfo> visibleTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
- final GroupedTaskInfo splitTaskInfo = visibleTasks.stream()
- .filter(taskInfo -> taskInfo.getType() == TYPE_SPLIT)
- .findFirst().orElse(null);
+ if (enableShellTopTaskTracking()) {
+ // TODO(346588978): This assumes default display as splitscreen is only currently there
+ final GroupedTaskInfo visibleTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
+ final GroupedTaskInfo splitTaskInfo =
+ visibleTasks != null && visibleTasks.isBaseType(TYPE_SPLIT)
+ ? visibleTasks.getBaseGroupedTask()
+ : null;
if (splitTaskInfo != null && splitTaskInfo.getSplitBounds() != null) {
return new int[] {
splitTaskInfo.getSplitBounds().leftTopTaskId,
@@ -317,24 +301,13 @@
* Dumps the list of tasks in top task tracker.
*/
public void dump(PrintWriter pw) {
- if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (!enableShellTopTaskTracking()) {
return;
}
- // TODO(346588978): This assumes default display for now
- final ArrayList<GroupedTaskInfo> displayTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
pw.println("TopTaskTracker:");
- pw.println(" tasks: [");
- for (GroupedTaskInfo taskInfo : displayTasks) {
- final TaskInfo info = taskInfo.getTaskInfo1();
- final boolean isExcluded = (info.baseIntent.getFlags()
- & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
- pw.println(" " + info.taskId + ": excluded=" + isExcluded
- + " visibleRequested=" + info.isVisibleRequested
- + " visible=" + info.isVisible
- + " " + info.baseIntent.getComponent());
- }
- pw.println(" ]");
+ mVisibleTasks.forEach((displayId, tasks) ->
+ pw.println(" visibleTasks(" + displayId + "): " + tasks));
}
/**
@@ -343,13 +316,12 @@
@NonNull
@UiThread
public CachedTaskInfo getCachedTopTask(boolean filterOnlyVisibleRecents) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
// TODO(346588978): Currently ignore filterOnlyVisibleRecents, but perhaps make this an
// explicit filter For things to ignore (ie. PIP/Bubbles/Assistant/etc/so that this is
// explicit)
- // TODO(346588978): This assumes default display for now (as does all of Launcher)
- final ArrayList<GroupedTaskInfo> displayTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
- return new CachedTaskInfo(new ArrayList<>(displayTasks));
+ // TODO(346588978): This assumes default display as gesture nav is only supported there
+ return new CachedTaskInfo(mVisibleTasks.get(DEFAULT_DISPLAY));
} else {
if (filterOnlyVisibleRecents) {
// Since we only know about the top most task, any filtering may not be applied on
@@ -374,6 +346,11 @@
}
}
+ private static boolean isHomeTask(TaskInfo task) {
+ return task != null && task.configuration.windowConfiguration
+ .getActivityType() == ACTIVITY_TYPE_HOME;
+ }
+
private static boolean isRecentsTask(TaskInfo task) {
return task != null && task.configuration.windowConfiguration
.getActivityType() == ACTIVITY_TYPE_RECENTS;
@@ -384,7 +361,6 @@
* during the lifecycle of the task.
*/
public static class CachedTaskInfo {
-
// Only used when enableShellTopTaskTracking() is disabled
@Nullable
private final TaskInfo mTopTask;
@@ -393,40 +369,48 @@
// Only used when enableShellTopTaskTracking() is enabled
@Nullable
- private final GroupedTaskInfo mTopGroupedTask;
- @Nullable
- private final ArrayList<GroupedTaskInfo> mVisibleTasks;
+ private final GroupedTaskInfo mVisibleTasks;
// Only used when enableShellTopTaskTracking() is enabled
- CachedTaskInfo(@NonNull ArrayList<GroupedTaskInfo> visibleTasks) {
+ CachedTaskInfo(@Nullable GroupedTaskInfo visibleTasks) {
mAllCachedTasks = null;
mTopTask = null;
mVisibleTasks = visibleTasks;
- mTopGroupedTask = !mVisibleTasks.isEmpty() ? mVisibleTasks.getFirst() : null;
}
// Only used when enableShellTopTaskTracking() is disabled
CachedTaskInfo(@NonNull List<TaskInfo> allCachedTasks) {
mVisibleTasks = null;
- mTopGroupedTask = null;
mAllCachedTasks = allCachedTasks;
mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0);
}
/**
- * @return The list of visible tasks
+ * Returns the "base" task that is used the as the representative running task of the set
+ * of tasks initially provided.
+ *
+ * Not for general use, as in other windowing modes (ie. split/desktop) the caller should
+ * not make assumptions about there being a single base task.
+ * TODO(346588978): Try to remove all usage of this if possible
*/
- public ArrayList<GroupedTaskInfo> getVisibleTasks() {
- return mVisibleTasks;
+ @Nullable
+ private TaskInfo getLegacyBaseTask() {
+ if (enableShellTopTaskTracking()) {
+ return mVisibleTasks != null
+ ? mVisibleTasks.getBaseGroupedTask().getTaskInfo1()
+ : null;
+ } else {
+ return mTopTask;
+ }
}
/**
- * @return The top task id
+ * Returns the top task id.
*/
public int getTaskId() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
// Callers should use topGroupedTaskContainsTask() instead
return INVALID_TASK_ID;
} else {
@@ -435,29 +419,58 @@
}
/**
- * @return Whether the top grouped task contains the given {@param taskId} if
- * Flags.enableShellTopTaskTracking() is true, otherwise it checks the top
- * task as reported from TaskStackListener.
+ * Returns the top grouped task ids if Flags.enableShellTopTaskTracking() is true, otherwise
+ * an empty array.
+ */
+ public int[] topGroupedTaskIds() {
+ if (enableShellTopTaskTracking()) {
+ if (mVisibleTasks == null) {
+ return new int[0];
+ }
+ List<TaskInfo> groupedTasks = mVisibleTasks.getTaskInfoList();
+ return groupedTasks.stream().mapToInt(
+ groupedTask -> groupedTask.taskId).toArray();
+ } else {
+ // Not used
+ return new int[0];
+ }
+ }
+
+ /**
+ * Returns whether the top grouped task contains the given {@param taskId} if
+ * Flags.enableShellTopTaskTracking() is true, otherwise it checks the top task as reported
+ * from TaskStackListener.
*/
public boolean topGroupedTaskContainsTask(int taskId) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- return mTopGroupedTask != null && mTopGroupedTask.containsTask(taskId);
+ if (enableShellTopTaskTracking()) {
+ return mVisibleTasks != null && mVisibleTasks.containsTask(taskId);
} else {
return mTopTask != null && mTopTask.taskId == taskId;
}
}
/**
- * Returns true if the root of the task chooser activity
+ * Returns true if this represents the task chooser activity
*/
public boolean isRootChooseActivity() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- // TODO(346588978): Update this to not make an assumption on a specific task info
- return mTopGroupedTask != null && ACTION_CHOOSER.equals(
- mTopGroupedTask.getTaskInfo1().baseIntent.getAction());
- } else {
- return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction());
- }
+ final TaskInfo baseTask = getLegacyBaseTask();
+ return baseTask != null && ACTION_CHOOSER.equals(baseTask.baseIntent.getAction());
+ }
+
+ /**
+ * Returns true if this represents the HOME activity type task
+ */
+ public boolean isHomeTask() {
+ final TaskInfo baseTask = getLegacyBaseTask();
+ return baseTask != null && TopTaskTracker.isHomeTask(baseTask);
+ }
+
+ /**
+ * Returns true if this represents the RECENTS activity type task
+ */
+ public boolean isRecentsTask() {
+ final TaskInfo baseTask = getLegacyBaseTask();
+ return baseTask != null && TopTaskTracker.isRecentsTask(baseTask);
}
/**
@@ -465,7 +478,7 @@
* is another running task that is not excluded from recents, returns that underlying task.
*/
public @Nullable CachedTaskInfo getVisibleNonExcludedTask() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (enableShellTopTaskTracking()) {
// Callers should not need this when the full set of visible tasks are provided
return null;
}
@@ -485,49 +498,16 @@
}
/**
- * Returns true if this represents the HOME activity type task
- */
- public boolean isHomeTask() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- // TODO(346588978): Update this to not make an assumption on a specific task info
- return mTopGroupedTask != null
- && mTopGroupedTask.getTaskInfo1().getActivityType() == ACTIVITY_TYPE_HOME;
- } else {
- return mTopTask != null && mTopTask.configuration.windowConfiguration
- .getActivityType() == ACTIVITY_TYPE_HOME;
- }
- }
-
- /**
- * Returns true if this represents the RECENTS activity type task
- */
- public boolean isRecentsTask() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- // TODO(346588978): Update this to not make an assumption on a specific task info
- return mTopGroupedTask != null
- && TopTaskTracker.isRecentsTask(mTopGroupedTask.getTaskInfo1());
- } else {
- return TopTaskTracker.isRecentsTask(mTopTask);
- }
- }
-
- /**
* Returns {@link Task} array which can be used as a placeholder until the true object
* is loaded by the model
*/
public Task[] getPlaceholderTasks() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- // TODO(346588978): Update this to return more than a single task once the callers
- // are refactored
- if (mVisibleTasks.isEmpty()) {
- return new Task[0];
- }
- final TaskInfo info = mVisibleTasks.getFirst().getTaskInfo1();
- return new Task[]{Task.from(new TaskKey(info), info, false)};
- } else {
- return mTopTask == null ? new Task[0]
- : new Task[]{Task.from(new TaskKey(mTopTask), mTopTask, false)};
- }
+ final TaskInfo baseTask = getLegacyBaseTask();
+ // TODO(346588978): Update this to return more than a single task once the callers
+ // are refactored
+ return baseTask == null
+ ? new Task[0]
+ : new Task[]{Task.from(new TaskKey(baseTask), baseTask, false)};
}
/**
@@ -535,13 +515,12 @@
* placeholder until the true object is loaded by the model
*/
public Task[] getSplitPlaceholderTasks(int[] taskIds) {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- if (mVisibleTasks.isEmpty()
- || mVisibleTasks.getFirst().getType() != TYPE_SPLIT) {
+ if (enableShellTopTaskTracking()) {
+ if (mVisibleTasks == null || !mVisibleTasks.isBaseType(TYPE_SPLIT)) {
return new Task[0];
}
- GroupedTaskInfo splitTask = mVisibleTasks.getFirst();
+ GroupedTaskInfo splitTask = mVisibleTasks.getBaseGroupedTask();
Task[] result = new Task[taskIds.length];
for (int i = 0; i < taskIds.length; i++) {
TaskInfo info = splitTask.getTaskById(taskIds[i]);
@@ -572,22 +551,11 @@
@Nullable
public String getPackageName() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- // TODO(346588978): Update this to not make an assumption on a specific task info
- if (mTopGroupedTask == null) {
- return null;
- }
- final TaskInfo info = mTopGroupedTask.getTaskInfo1();
- if (info.baseActivity == null) {
- return null;
- }
- return info.baseActivity.getPackageName();
- } else {
- if (mTopTask == null || mTopTask.baseActivity == null) {
- return null;
- }
- return mTopTask.baseActivity.getPackageName();
+ final TaskInfo baseTask = getLegacyBaseTask();
+ if (baseTask == null || baseTask.baseActivity == null) {
+ return null;
}
+ return baseTask.baseActivity.getPackageName();
}
}
}
diff --git a/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt b/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt
new file mode 100644
index 0000000..3823100
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.domain.model
+
+import android.graphics.drawable.Drawable
+import com.android.systemui.shared.recents.model.ThumbnailData
+
+/**
+ * Data class representing a task in the application.
+ *
+ * This class holds the essential information about a task, including its unique identifier, display
+ * title, associated icon, optional thumbnail data, and background color.
+ *
+ * @property id The unique identifier for this task. Must be an integer.
+ * @property title The display title of the task.
+ * @property titleDescription A content description of the task.
+ * @property icon An optional drawable resource representing an icon for the task. Can be null if no
+ * icon is required.
+ * @property thumbnail An optional [ThumbnailData] object containing thumbnail information. Can be
+ * null if no thumbnail is needed.
+ * @property backgroundColor The background color of the task, represented as an integer color
+ * value.
+ * @property isLocked Indicates whether the [Task] is locked.
+ */
+data class TaskModel(
+ val id: TaskId,
+ val title: String,
+ val titleDescription: String?,
+ val icon: Drawable?,
+ val thumbnail: ThumbnailData?,
+ val backgroundColor: Int,
+ val isLocked: Boolean,
+)
+
+typealias TaskId = Int
diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCase.kt
new file mode 100644
index 0000000..a60144b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCase.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.domain.usecase
+
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.domain.model.TaskModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class GetTaskUseCase(private val repository: RecentTasksRepository) {
+ operator fun invoke(taskId: Int): Flow<TaskModel?> =
+ repository.getTaskDataById(taskId).map { task ->
+ if (task != null) {
+ TaskModel(
+ id = task.key.id,
+ title = task.title,
+ titleDescription = task.titleDescription,
+ icon = task.icon,
+ thumbnail = task.thumbnail,
+ backgroundColor = task.colorBackground,
+ isLocked = task.isLocked,
+ )
+ } else {
+ null
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
new file mode 100644
index 0000000..5f98479
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+import com.android.systemui.shared.recents.model.ThumbnailData
+
+/**
+ * This class represents the UI state to be consumed by TaskView, GroupTaskView and DesktopTaskView.
+ * Data class representing the state of a list of tasks.
+ *
+ * This class encapsulates a list of [TaskTileUiState] objects, along with a flag indicating whether
+ * the data is being used for a live tile display.
+ *
+ * @property tasks The list of [TaskTileUiState] objects representing the individual tasks.
+ * @property isLiveTile Indicates whether this data is intended for a live tile. If `true`, the
+ * running app will be displayed instead of the thumbnail.
+ */
+data class TaskTileUiState(val tasks: List<TaskData>, val isLiveTile: Boolean)
+
+sealed interface TaskData {
+ /** When no data was found for the TaskId provided */
+ data class NoData(val taskId: Int) : TaskData
+
+ /**
+ * This class provides UI information related to a Task (App) to be displayed within a TaskView.
+ *
+ * @property taskId Identifier of the task
+ * @property title App title
+ * @property icon App icon
+ * @property thumbnailData Information related to the last snapshot retrieved from the app
+ * @property backgroundColor The background color of the task.
+ * @property isLocked Indicates whether the task is locked or not.
+ */
+ data class Data(
+ val taskId: Int,
+ val title: String,
+ val icon: Drawable?,
+ val thumbnailData: ThumbnailData?,
+ val backgroundColor: Int,
+ val isLocked: Boolean,
+ ) : TaskData
+}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
new file mode 100644
index 0000000..2e51a8a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.ui.viewmodel
+
+import android.util.Log
+import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.quickstep.recents.domain.model.TaskId
+import com.android.quickstep.recents.domain.model.TaskModel
+import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * ViewModel used for [com.android.quickstep.views.TaskView],
+ * [com.android.quickstep.views.DesktopTaskView] and [com.android.quickstep.views.GroupedTaskView].
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class TaskViewModel(
+ recentsViewData: RecentsViewData,
+ private val getTaskUseCase: GetTaskUseCase,
+ dispatcherProvider: DispatcherProvider,
+) {
+ private var taskIds = MutableStateFlow(emptySet<Int>())
+
+ private val isLiveTile =
+ combine(
+ taskIds,
+ recentsViewData.runningTaskIds,
+ recentsViewData.runningTaskShowScreenshot,
+ ) { taskIds, runningTaskIds, runningTaskShowScreenshot ->
+ runningTaskIds == taskIds && !runningTaskShowScreenshot
+ }
+ .distinctUntilChanged()
+
+ val state: Flow<TaskTileUiState> =
+ taskIds
+ .flatMapLatest { ids ->
+ // Combine Tasks requests
+ combine(
+ ids.map { id -> getTaskUseCase(id).map { taskModel -> id to taskModel } },
+ ::mapToUiState,
+ )
+ }
+ .combine(isLiveTile) { tasks, isLiveTile -> TaskTileUiState(tasks, isLiveTile) }
+ .flowOn(dispatcherProvider.background)
+
+ fun bind(vararg taskId: TaskId) {
+ Log.d(TAG, "bind: $taskId")
+ taskIds.value = taskId.toSet()
+ }
+
+ private fun mapToUiState(result: Array<Pair<TaskId, TaskModel?>>): List<TaskData> =
+ result.map { mapToUiState(it.first, it.second) }
+
+ private fun mapToUiState(taskId: TaskId, result: TaskModel?): TaskData =
+ result?.let {
+ TaskData.Data(
+ taskId = taskId,
+ title = result.title,
+ icon = result.icon,
+ thumbnailData = result.thumbnail,
+ backgroundColor = result.backgroundColor,
+ isLocked = result.isLocked,
+ )
+ } ?: TaskData.NoData(taskId)
+
+ private companion object {
+ const val TAG = "TaskViewModel"
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index dc849f3..99df84c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -613,6 +613,7 @@
borderEnabled = false
hoverBorderVisible = false
taskViewId = UNBOUND_TASK_VIEW_ID
+ // TODO(b/390583187): Clean the components UI State when TaskView is recycled.
taskContainers.forEach { it.destroy() }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index 1c9ce0b..35af29f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -58,10 +58,10 @@
tasks.value.map {
it.apply {
thumbnail = thumbnailDataMap[it.key.id]
- taskIconDataMap[it.key.id].let { data ->
- title = data?.title
- titleDescription = data?.titleDescription
- icon = data?.icon
+ taskIconDataMap[it.key.id]?.let { data ->
+ title = data.title
+ titleDescription = data.titleDescription
+ icon = data.icon
}
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt
new file mode 100644
index 0000000..b036bce
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.domain.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.drawable.ShapeDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.domain.model.TaskModel
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class GetTaskUseCaseTest {
+ private val unconfinedTestDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(unconfinedTestDispatcher)
+
+ private val tasksRepository = FakeTasksRepository()
+ private val sut = GetTaskUseCase(repository = tasksRepository)
+
+ @Before
+ fun setUp() {
+ tasksRepository.seedTasks(listOf(TASK_1))
+ }
+
+ @Test
+ fun taskNotSeeded_returnsNull() =
+ testScope.runTest {
+ val result = sut.invoke(NOT_FOUND_TASK_ID).firstOrNull()
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun taskNotVisible_returnsNull() =
+ testScope.runTest {
+ val result = sut.invoke(TASK_1_ID).firstOrNull()
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun taskVisible_returnsData() =
+ testScope.runTest {
+ tasksRepository.setVisibleTasks(setOf(TASK_1_ID))
+ val expectedResult =
+ TaskModel(
+ id = TASK_1_ID,
+ title = "Title $TASK_1_ID",
+ titleDescription = "Content Description $TASK_1_ID",
+ icon = TASK_1_ICON,
+ thumbnail = null,
+ backgroundColor = Color.BLACK,
+ isLocked = false,
+ )
+ val result = sut.invoke(TASK_1_ID).firstOrNull()
+ assertThat(result).isEqualTo(expectedResult)
+ }
+
+ private companion object {
+ const val NOT_FOUND_TASK_ID = 404
+ private const val TASK_1_ID = 1
+ private val TASK_1_ICON = ShapeDrawable()
+ private val TASK_1 =
+ Task(
+ Task.TaskKey(
+ /* id = */ TASK_1_ID,
+ /* windowingMode = */ 0,
+ /* intent = */ Intent(),
+ /* sourceComponent = */ ComponentName("", ""),
+ /* userId = */ 0,
+ /* lastActiveTime = */ 2000,
+ )
+ )
+ .apply {
+ title = "Title 1"
+ titleDescription = "Content Description 1"
+ colorBackground = Color.BLACK
+ icon = TASK_1_ICON
+ thumbnail = null
+ isLocked = false
+ }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
new file mode 100644
index 0000000..54a27e9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.ui.viewmodel
+
+import android.graphics.Color
+import android.graphics.drawable.ShapeDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.domain.model.TaskModel
+import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class TaskViewModelTest {
+ private val unconfinedTestDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(unconfinedTestDispatcher)
+
+ private val recentsViewData = RecentsViewData()
+ private val getTaskUseCase = mock<GetTaskUseCase>()
+ private val sut =
+ TaskViewModel(
+ recentsViewData = recentsViewData,
+ getTaskUseCase = getTaskUseCase,
+ dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
+ )
+
+ @Before
+ fun setUp() {
+ whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
+ whenever(getTaskUseCase.invoke(TASK_MODEL_2.id)).thenReturn(flow { emit(TASK_MODEL_2) })
+ whenever(getTaskUseCase.invoke(TASK_MODEL_3.id)).thenReturn(flow { emit(TASK_MODEL_3) })
+ whenever(getTaskUseCase.invoke(INVALID_TASK_ID)).thenReturn(flow { emit(null) })
+ recentsViewData.runningTaskIds.value = emptySet()
+ }
+
+ @Test
+ fun singleTaskRetrieved_when_validTaskId() =
+ testScope.runTest {
+ sut.bind(TASK_MODEL_1.id)
+ val expectedResult = TaskTileUiState(listOf(TASK_MODEL_1.toUiState()), false)
+ assertThat(sut.state.first()).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun multipleTasksRetrieved_when_validTaskIds() =
+ testScope.runTest {
+ sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id, INVALID_TASK_ID)
+ val expectedResult =
+ TaskTileUiState(
+ tasks =
+ listOf(
+ TASK_MODEL_1.toUiState(),
+ TASK_MODEL_2.toUiState(),
+ TASK_MODEL_3.toUiState(),
+ TaskData.NoData(INVALID_TASK_ID),
+ ),
+ isLiveTile = false,
+ )
+ assertThat(sut.state.first()).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun isLiveTile_when_runningTasksMatchTasks() =
+ testScope.runTest {
+ recentsViewData.runningTaskShowScreenshot.value = false
+ recentsViewData.runningTaskIds.value =
+ setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id)
+ sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id)
+ val expectedResult =
+ TaskTileUiState(
+ tasks =
+ listOf(
+ TASK_MODEL_1.toUiState(),
+ TASK_MODEL_2.toUiState(),
+ TASK_MODEL_3.toUiState(),
+ ),
+ isLiveTile = true,
+ )
+ assertThat(sut.state.first()).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun isNotLiveTile_when_runningTaskShowScreenshotIsTrue() =
+ testScope.runTest {
+ recentsViewData.runningTaskShowScreenshot.value = true
+ recentsViewData.runningTaskIds.value =
+ setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id)
+ sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id)
+ val expectedResult =
+ TaskTileUiState(
+ tasks =
+ listOf(
+ TASK_MODEL_1.toUiState(),
+ TASK_MODEL_2.toUiState(),
+ TASK_MODEL_3.toUiState(),
+ ),
+ isLiveTile = false,
+ )
+ assertThat(sut.state.first()).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun isNotLiveTile_when_runningTasksMatchPartialTasks_lessRunningTasks() =
+ testScope.runTest {
+ recentsViewData.runningTaskShowScreenshot.value = false
+ recentsViewData.runningTaskIds.value = setOf(TASK_MODEL_1.id, TASK_MODEL_2.id)
+ sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id)
+ val expectedResult =
+ TaskTileUiState(
+ tasks =
+ listOf(
+ TASK_MODEL_1.toUiState(),
+ TASK_MODEL_2.toUiState(),
+ TASK_MODEL_3.toUiState(),
+ ),
+ isLiveTile = false,
+ )
+ assertThat(sut.state.first()).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun isNotLiveTile_when_runningTasksMatchPartialTasks_moreRunningTasks() =
+ testScope.runTest {
+ recentsViewData.runningTaskShowScreenshot.value = false
+ recentsViewData.runningTaskIds.value =
+ setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id)
+ sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id)
+ val expectedResult =
+ TaskTileUiState(
+ tasks = listOf(TASK_MODEL_1.toUiState(), TASK_MODEL_2.toUiState()),
+ isLiveTile = false,
+ )
+ assertThat(sut.state.first()).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun noDataAvailable_when_InvalidTaskId() =
+ testScope.runTest {
+ sut.bind(INVALID_TASK_ID)
+ val expectedResult =
+ TaskTileUiState(listOf(TaskData.NoData(INVALID_TASK_ID)), isLiveTile = false)
+ assertThat(sut.state.first()).isEqualTo(expectedResult)
+ }
+
+ private fun TaskModel.toUiState() =
+ TaskData.Data(
+ taskId = id,
+ title = title,
+ icon = icon!!,
+ thumbnailData = thumbnail,
+ backgroundColor = backgroundColor,
+ isLocked = isLocked,
+ )
+
+ companion object {
+ const val INVALID_TASK_ID = -1
+ val TASK_MODEL_1 =
+ TaskModel(
+ 1,
+ "Title 1",
+ "Content Description 1",
+ ShapeDrawable(),
+ ThumbnailData(),
+ Color.BLACK,
+ false,
+ )
+ val TASK_MODEL_2 =
+ TaskModel(
+ 2,
+ "Title 2",
+ "Content Description 2",
+ ShapeDrawable(),
+ ThumbnailData(),
+ Color.RED,
+ true,
+ )
+ val TASK_MODEL_3 =
+ TaskModel(
+ 3,
+ "Title 3",
+ "Content Description 3",
+ ShapeDrawable(),
+ ThumbnailData(),
+ Color.BLUE,
+ false,
+ )
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 73aa460..0044631 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -76,7 +76,7 @@
assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
}
- companion object {
+ private companion object {
const val TASK_ID = 0
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
index 67a0ee4..3f7c85c 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -127,11 +127,11 @@
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@@ -141,11 +141,11 @@
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@@ -155,11 +155,11 @@
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@@ -169,11 +169,11 @@
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@@ -184,11 +184,11 @@
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
doReturn(true).when(mSpyFolderView).isOpen();
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@@ -199,10 +199,10 @@
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
doReturn(true).when(mSpyFolderView).isOpen();
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
}
@Test
@@ -210,10 +210,10 @@
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
}
@Test
@@ -221,11 +221,11 @@
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@@ -235,11 +235,11 @@
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@@ -250,11 +250,11 @@
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true);
- boolean hoverHandled =
+ boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
- assertThat(hoverHandled).isTrue();
+ assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 4e44324..753b2e2 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -126,7 +126,7 @@
/** Type of popups that should get exclusive accessibility focus. */
public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER
& ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP
- & ~TYPE_WIDGET_RESIZE_FRAME & ~TYPE_ONE_GRID_MIGRATION_EDU;
+ & ~TYPE_WIDGET_RESIZE_FRAME & ~TYPE_ONE_GRID_MIGRATION_EDU & ~TYPE_ON_BOARD_POPUP;
// These view all have particular operation associated with swipe down interaction.
public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET |
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 175d6ec..468cee8 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -261,6 +261,13 @@
return count;
}
+ private void addProfileId(XmlPullParser parser) {
+ Long profileId = mUserTypeToSerial.get(getAttributeValue(parser, ATTR_USER_TYPE));
+ if (profileId != null) {
+ mValues.put(Favorites.PROFILE_ID, profileId);
+ }
+ }
+
/**
* Parses container and screenId attribute from the current tag, and puts it in the out.
* @param out array of size 2.
@@ -305,10 +312,6 @@
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount));
mValues.put(Favorites.CELLY,
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount));
- Long profileId = mUserTypeToSerial.get(getAttributeValue(parser, ATTR_USER_TYPE));
- if (profileId != null) {
- mValues.put(Favorites.PROFILE_ID, profileId);
- }
TagParser tagParser = tagParserMap.get(parser.getName());
if (tagParser == null) {
@@ -382,7 +385,7 @@
public int parseAndAdd(XmlPullParser parser) {
final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
-
+ addProfileId(parser);
if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
ActivityInfo info;
try {
@@ -431,6 +434,7 @@
public int parseAndAdd(XmlPullParser parser) {
final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
+ addProfileId(parser);
if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) {
if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component");
return -1;
@@ -452,7 +456,7 @@
public int parseAndAdd(XmlPullParser parser) {
final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
final String shortcutId = getAttributeValue(parser, ATTR_SHORTCUT_ID);
-
+ addProfileId(parser);
try {
LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
launcherApps.pinShortcuts(packageName, Collections.singletonList(shortcutId),
@@ -482,13 +486,13 @@
public ComponentName getComponentName(XmlPullParser parser) {
final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
+ addProfileId(parser);
if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) {
return null;
}
return new ComponentName(packageName, className);
}
-
@Override
public int parseAndAdd(XmlPullParser parser)
throws XmlPullParserException, IOException {
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index afc7b2e..315096c 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -1239,6 +1239,7 @@
mIcon = icon;
if (mIcon != null) {
mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
+ mIcon.setHoverScaleEnabledForDisplay(mDisplay != DISPLAY_TASKBAR);
}
}
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 329d9df..7e08c6e 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -431,7 +431,7 @@
&& !(itemInfo instanceof WorkspaceItemInfo)) {
return null;
}
- return new BubbleShortcut(activity, itemInfo, originalView);
+ return new BubbleShortcut<>(activity, itemInfo, originalView);
};
public interface BubbleActivityStarter {
@@ -439,7 +439,7 @@
void showShortcutBubble(ShortcutInfo info);
/** Tell SysUI to show the provided intent in a bubble. */
- void showAppBubble(Intent intent);
+ void showAppBubble(Intent intent, UserHandle user);
}
public static class BubbleShortcut<T extends ActivityContext> extends SystemShortcut<T> {
@@ -476,7 +476,7 @@
if (intent.getPackage() == null) {
intent.setPackage(mItemInfo.getTargetPackage());
}
- mStarter.showAppBubble(intent);
+ mStarter.showAppBubble(intent, mItemInfo.user);
} else {
Log.w(TAG, "unable to bubble, no intent: " + mItemInfo);
}
diff --git a/src/com/android/launcher3/util/LayoutImportExportHelper.kt b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
index 4033f60..0df9dae 100644
--- a/src/com/android/launcher3/util/LayoutImportExportHelper.kt
+++ b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
@@ -136,4 +136,4 @@
)
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
index bbcc6a8..033cfb0 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
@@ -63,5 +63,12 @@
return new SplitScreenMenuItem(mLauncher, menuItem);
}
+ /** Returns the Bubble menu item. */
+ public BubbleMenuItem getBubbleMenuItem() {
+ final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
+ AppIcon.getMenuItemSelector("Bubble", mLauncher));
+ return new BubbleMenuItem(mLauncher, menuItem);
+ }
+
protected abstract AppIconMenuItem createMenuItem(UiObject2 menuItem);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/BubbleMenuItem.kt b/tests/tapl/com/android/launcher3/tapl/BubbleMenuItem.kt
new file mode 100644
index 0000000..77391f1
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/BubbleMenuItem.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2025 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.tapl
+
+import androidx.test.uiautomator.UiObject2
+
+/**
+ * A class representing the Bubble menu item in the app long-press menu, which moves the app into a
+ * bubble.
+ */
+class BubbleMenuItem(
+ private val launcher: LauncherInstrumentation,
+ private val uiObject: UiObject2,
+) {
+
+ fun click() {
+ launcher.addContextLayer("want to create bubble from app long-press menu").use {
+ LauncherInstrumentation.log(
+ "clicking on bubble menu item ${uiObject.visibleCenter} in ${
+ launcher.getVisibleBounds(
+ uiObject
+ )
+ }"
+ )
+ launcher.clickLauncherObject(uiObject)
+ }
+ }
+}