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)
+        }
+    }
+}