Show running apps in Taskbar when in Desktop Mode

In Desktop Mode: show currently running (Desktop) apps in Taskbar

Bug: 332504528
Test: DesktopTaskbarRunningAppsControllerTest
Flag: ACONFIG com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps DEVELOPMENT

Change-Id: I10dec022f4b16562e81d7cbd5f9ce835474e9d20
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 74376c8..5ac5761 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -37,6 +37,9 @@
 import com.android.quickstep.views.DesktopAppSelectView;
 import com.android.wm.shell.desktopmode.IDesktopTaskListener;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Controls the visibility of the workspace and the resumed / paused state when desktop mode
  * is enabled.
@@ -48,6 +51,7 @@
     private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean(
             "persist.wm.debug.desktop_stashing", false);
     private final Launcher mLauncher;
+    private final Set<DesktopVisibilityListener> mDesktopVisibilityListeners = new HashSet<>();
 
     private int mVisibleDesktopTasksCount;
     private boolean mInOverviewState;
@@ -127,6 +131,16 @@
         return mVisibleDesktopTasksCount;
     }
 
+    /** Registers a listener for Desktop Mode visibility updates. */
+    public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) {
+        mDesktopVisibilityListeners.add(listener);
+    }
+
+    /** Removes a previously registered Desktop Mode visibility listener. */
+    public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) {
+        mDesktopVisibilityListeners.remove(listener);
+    }
+
     /**
      * Sets the number of desktop windows that are visible and updates launcher visibility based on
      * it.
@@ -140,7 +154,12 @@
         if (visibleTasksCount != mVisibleDesktopTasksCount) {
             final boolean wasVisible = mVisibleDesktopTasksCount > 0;
             final boolean isVisible = visibleTasksCount > 0;
+            final boolean wereDesktopTasksVisibleBefore = areDesktopTasksVisible();
             mVisibleDesktopTasksCount = visibleTasksCount;
+            final boolean areDesktopTasksVisibleNow = areDesktopTasksVisible();
+            if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
+                notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
+            }
 
             if (!enableDesktopWindowingWallpaperActivity() && wasVisible != isVisible) {
                 // TODO: b/333533253 - Remove after flag rollout
@@ -179,15 +198,22 @@
                     + " currentValue=" + mInOverviewState);
         }
         if (overviewStateEnabled != mInOverviewState) {
+            final boolean wereDesktopTasksVisibleBefore = areDesktopTasksVisible();
             mInOverviewState = overviewStateEnabled;
+            final boolean areDesktopTasksVisibleNow = areDesktopTasksVisible();
+            if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
+                notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
+            }
+
             if (enableDesktopWindowingWallpaperActivity()) {
                 return;
             }
             // TODO: b/333533253 - Clean up after flag rollout
+
             if (mInOverviewState) {
                 setLauncherViewsVisibility(View.VISIBLE);
                 markLauncherResumed();
-            } else if (areDesktopTasksVisible() && !mGestureInProgress) {
+            } else if (areDesktopTasksVisibleNow && !mGestureInProgress) {
                 // Switching out of overview state and gesture finished.
                 // If desktop tasks are still visible, hide launcher again.
                 setLauncherViewsVisibility(View.INVISIBLE);
@@ -196,6 +222,15 @@
         }
     }
 
+    private void notifyDesktopVisibilityListeners(boolean areDesktopTasksVisible) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyDesktopVisibilityListeners: visible=" + areDesktopTasksVisible);
+        }
+        for (DesktopVisibilityListener listener : mDesktopVisibilityListeners) {
+            listener.onDesktopVisibilityChanged(areDesktopTasksVisible);
+        }
+    }
+
     /**
      * TODO: b/333533253 - Remove after flag rollout
      */
@@ -359,4 +394,14 @@
         mSelectAppToast.hide();
         mSelectAppToast = null;
     }
+
+    /** A listener for when the user enters/exits Desktop Mode. */
+    public interface DesktopVisibilityListener {
+        /**
+         * Callback for when the user enters or exits Desktop Mode
+         *
+         * @param visible whether Desktop Mode is now visible
+         */
+        void onDesktopVisibilityChanged(boolean visible);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
new file mode 100644
index 0000000..f665e21
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration
+import android.util.Log
+import android.util.SparseArray
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.valueIterator
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.quickstep.RecentsModel
+import kotlin.collections.filterNotNull
+
+/**
+ * Shows running apps when in Desktop Mode.
+ *
+ * Users can enter and exit Desktop Mode at run-time, meaning this class falls back to the default
+ * recent-apps behaviour when outside of Desktop Mode.
+ *
+ * This class should only be used if
+ * [com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps] is enabled.
+ */
+class DesktopTaskbarRunningAppsController(
+    private val recentsModel: RecentsModel,
+    private val desktopVisibilityController: DesktopVisibilityController?,
+) : TaskbarRecentAppsController() {
+
+    private var apps: Array<AppInfo>? = null
+    private var allRunningDesktopAppInfos: List<AppInfo>? = null
+    private var runningDesktopAppInfosExceptHotseatItems: List<ItemInfo>? = null
+
+    private val isInDesktopMode: Boolean
+        get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
+
+    override fun onDestroy() {
+        super.onDestroy()
+        apps = null
+    }
+
+    @VisibleForTesting
+    public override fun setApps(apps: Array<AppInfo>?) {
+        this.apps = apps
+    }
+
+    override fun isEnabled() = true
+
+    @VisibleForTesting
+    public override fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo>?): Array<ItemInfo>? {
+        val actualHotseatItems = hotseatItems ?: return super.updateHotseatItemInfos(null)
+        if (!isInDesktopMode) {
+            Log.d(TAG, "updateHotseatItemInfos: not in Desktop Mode")
+            return hotseatItems
+        }
+        val newHotseatItemInfos =
+            actualHotseatItems
+                // Ignore predicted apps - we show running apps instead
+                .filter { itemInfo -> !itemInfo.isPredictedItem }
+                .toMutableList()
+        val runningDesktopAppInfos =
+            runningDesktopAppInfosExceptHotseatItems ?: return newHotseatItemInfos.toTypedArray()
+        newHotseatItemInfos.addAll(runningDesktopAppInfos)
+        return newHotseatItemInfos.toTypedArray()
+    }
+
+    @VisibleForTesting
+    public override fun updateRunningApps(hotseatItems: SparseArray<ItemInfo>?) {
+        if (!isInDesktopMode) {
+            Log.d(TAG, "updateRunningApps: not in Desktop Mode")
+            mControllers.taskbarViewController.commitRunningAppsToUI()
+            return
+        }
+        val allRunningDesktopAppInfos = getRunningDesktopAppInfos()
+        this.allRunningDesktopAppInfos = allRunningDesktopAppInfos
+        runningDesktopAppInfosExceptHotseatItems =
+            hotseatItems?.let {
+                getRunningDesktopAppInfosExceptHotseatApps(allRunningDesktopAppInfos, it.toList())
+            }
+
+        mControllers.taskbarViewController.commitRunningAppsToUI()
+    }
+
+    private fun getRunningDesktopAppInfosExceptHotseatApps(
+        allRunningDesktopAppInfos: List<AppInfo>,
+        hotseatItems: List<ItemInfo>
+    ): List<ItemInfo> {
+        val hotseatPackages = hotseatItems.map { it.targetPackage }
+        return allRunningDesktopAppInfos
+            .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) }
+            .map { WorkspaceItemInfo(it) }
+    }
+
+    private fun getRunningDesktopAppInfos(): List<AppInfo> {
+        return getAppInfosFromRunningTasks(
+            recentsModel.runningTasks
+                .filter { taskInfo: RunningTaskInfo ->
+                    taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM
+                }
+                .toList()
+        )
+    }
+
+    // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos
+    private fun getAppInfosFromRunningTasks(tasks: List<RunningTaskInfo>): List<AppInfo> {
+        // Early return if apps is empty, since we then have no AppInfo to compare to
+        if (apps == null) {
+            return emptyList()
+        }
+        val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull()
+        return packageNames
+            .map { packageName -> apps?.find { app -> packageName == app.targetPackage } }
+            .filterNotNull()
+    }
+
+    private fun <E> SparseArray<E>.toList(): List<E> {
+        return valueIterator().asSequence().toList()
+    }
+
+    companion object {
+        private const val TAG = "TabletDesktopTaskbarRunningAppsController"
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 49d4afe..679528b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -41,6 +41,8 @@
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
+import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
 
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
@@ -127,7 +129,9 @@
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.ViewCache;
 import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.NavHandle;
+import com.android.quickstep.RecentsModel;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
@@ -244,7 +248,7 @@
 
         mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this);
 
-        final boolean isDesktopMode = getPackageManager().hasSystemFeature(FEATURE_PC);
+        final boolean isPcMode = getPackageManager().hasSystemFeature(FEATURE_PC);
 
         // If Bubble bar is present, TaskbarControllers depends on it so build it first.
         Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
@@ -276,7 +280,7 @@
         mControllers = new TaskbarControllers(this,
                 new TaskbarDragController(this),
                 buttonController,
-                isDesktopMode
+                isPcMode
                         ? new DesktopNavbarButtonsViewController(this, mNavigationBarPanelContext,
                                 navButtonsView)
                         : new NavbarButtonsViewController(this, mNavigationBarPanelContext,
@@ -301,9 +305,7 @@
                 new VoiceInteractionWindowController(this),
                 new TaskbarTranslationController(this),
                 new TaskbarSpringOnStashController(this),
-                isDesktopMode
-                        ? new DesktopTaskbarRecentAppsController(this)
-                        : TaskbarRecentAppsController.DEFAULT,
+                createTaskbarRecentAppsController(isPcMode),
                 new TaskbarEduTooltipController(this),
                 new KeyboardQuickSwitchController(),
                 new TaskbarPinningController(this),
@@ -312,6 +314,18 @@
         mLauncherPrefs = LauncherPrefs.get(this);
     }
 
+    private TaskbarRecentAppsController createTaskbarRecentAppsController(boolean isPcMode) {
+        if (isPcMode) return new DesktopTaskbarRecentAppsController(this);
+        // TODO(b/335401172): unify DesktopMode checks in Launcher
+        final boolean showRunningAppsInDesktopMode = enableDesktopWindowingMode()
+                && enableDesktopWindowingTaskbarRunningApps();
+        return showRunningAppsInDesktopMode
+                        ? new DesktopTaskbarRunningAppsController(
+                                RecentsModel.INSTANCE.get(this),
+                                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController())
+                        : TaskbarRecentAppsController.DEFAULT;
+    }
+
     /** Updates {@link DeviceProfile} instances for any Taskbar windows. */
     public void updateDeviceProfile(DeviceProfile launcherDp) {
         applyDeviceProfile(launcherDp);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 14d46d1..6c84f80 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -15,6 +15,9 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
+
 import android.util.SparseArray;
 import android.view.View;
 
@@ -26,6 +29,7 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -33,6 +37,7 @@
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.RecentsModel;
 
 import java.io.PrintWriter;
@@ -62,6 +67,8 @@
     // Used to defer any UI updates during the SUW unstash animation.
     private boolean mDeferUpdatesForSUW;
     private Runnable mDeferredUpdates;
+    private DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener =
+            visible -> updateRunningApps();
 
     public TaskbarModelCallbacks(
             TaskbarActivityContext context, TaskbarView container) {
@@ -73,6 +80,15 @@
         mControllers = controllers;
         if (mControllers.taskbarRecentAppsController.isEnabled()) {
             RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this);
+
+            if (shouldShowRunningAppsInDesktopMode()) {
+                DesktopVisibilityController desktopVisibilityController =
+                        LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+                if (desktopVisibilityController != null) {
+                    desktopVisibilityController.registerDesktopVisibilityListener(
+                            mDesktopVisibilityListener);
+                }
+            }
         }
     }
 
@@ -81,6 +97,20 @@
      */
     public void unregisterListeners() {
         RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener();
+
+        if (shouldShowRunningAppsInDesktopMode()) {
+            DesktopVisibilityController desktopVisibilityController =
+                    LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+            if (desktopVisibilityController != null) {
+                desktopVisibilityController.unregisterDesktopVisibilityListener(
+                        mDesktopVisibilityListener);
+            }
+        }
+    }
+
+    private boolean shouldShowRunningAppsInDesktopMode() {
+        // TODO(b/335401172): unify DesktopMode checks in Launcher
+        return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 30bb863..edf05b5 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
+import static android.content.pm.PackageManager.FEATURE_PC;
 
 import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -23,6 +24,8 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
+import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -32,7 +35,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -1366,8 +1368,7 @@
      * Gets the set of running tasks.
      */
     public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks(int numTasks) {
-        if (mRecentTasks != null
-                && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC)) {
+        if (mRecentTasks != null && shouldEnableRunningTasksForDesktopMode()) {
             try {
                 return new ArrayList<>(Arrays.asList(mRecentTasks.getRunningTasks(numTasks)));
             } catch (RemoteException e) {
@@ -1377,6 +1378,12 @@
         return new ArrayList<>();
     }
 
+    private boolean shouldEnableRunningTasksForDesktopMode() {
+        // TODO(b/335401172): unify DesktopMode checks in Launcher
+        return (enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps())
+                || mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
+    }
+
     private boolean handleMessageAsync(Message msg) {
         switch (msg.what) {
             case MSG_SET_SHELF_HEIGHT:
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
new file mode 100644
index 0000000..93eefe2
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Process
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.util.SparseArray
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.quickstep.RecentsModel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidTestingRunner::class)
+class DesktopTaskbarRunningAppsControllerTest : TaskbarBaseTestCase() {
+
+    @get:Rule val mockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var mockRecentsModel: RecentsModel
+    @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
+
+    private var nextTaskId: Int = 500
+
+    private lateinit var taskbarRunningAppsController: DesktopTaskbarRunningAppsController
+    private lateinit var userHandle: UserHandle
+
+    @Before
+    fun setUp() {
+        super.setup()
+        userHandle = Process.myUserHandle()
+        taskbarRunningAppsController =
+            DesktopTaskbarRunningAppsController(mockRecentsModel, mockDesktopVisibilityController)
+        taskbarRunningAppsController.init(taskbarControllers)
+        taskbarRunningAppsController.setApps(
+            ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
+        )
+    }
+
+    @Test
+    fun updateHotseatItemInfos_null_returnsNull() {
+        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(/* hotseatItems= */ null))
+            .isNull()
+    }
+
+    @Test
+    fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() {
+        setInDesktopMode(false)
+        val hotseatItems =
+            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
+                .toTypedArray()
+
+        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems))
+            .isEqualTo(hotseatItems)
+    }
+
+    @Test
+    fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() {
+        setInDesktopMode(false)
+        val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+        val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
+        val runningTasks =
+            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
+        taskbarRunningAppsController.updateRunningApps(createSparseArray(hotseatItems))
+
+        val newHotseatItems =
+            taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+
+        assertThat(newHotseatItems?.map { it.targetPackage }).isEqualTo(hotseatPackages)
+    }
+
+    @Test
+    fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() {
+        setInDesktopMode(true)
+        val hotseatItems: Array<ItemInfo> =
+            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
+                .toTypedArray()
+
+        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems))
+            .isEqualTo(hotseatItems)
+    }
+
+    @Test
+    fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() {
+        setInDesktopMode(true)
+        val hotseatItems =
+            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
+        val runningTasks =
+            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
+        taskbarRunningAppsController.updateRunningApps(createSparseArray(hotseatItems))
+
+        val newHotseatItems =
+            taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+
+        val expectedPackages =
+            listOf(
+                HOTSEAT_PACKAGE_1,
+                HOTSEAT_PACKAGE_2,
+                RUNNING_APP_PACKAGE_1,
+                RUNNING_APP_PACKAGE_2,
+            )
+        assertThat(newHotseatItems?.map { it.targetPackage }).isEqualTo(expectedPackages)
+    }
+
+    @Test
+    fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() {
+        setInDesktopMode(true)
+        val hotseatItems =
+            createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
+        val runningTasks =
+            createDesktopTasksFromPackageNames(
+                listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+            )
+        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
+        taskbarRunningAppsController.updateRunningApps(createSparseArray(hotseatItems))
+
+        val newHotseatItems =
+            taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+
+        val expectedPackages =
+            listOf(
+                HOTSEAT_PACKAGE_1,
+                HOTSEAT_PACKAGE_2,
+                RUNNING_APP_PACKAGE_1,
+                RUNNING_APP_PACKAGE_2,
+            )
+        assertThat(newHotseatItems?.map { it.targetPackage }).isEqualTo(expectedPackages)
+    }
+
+    private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
+        return packageNames.map { createTestAppInfo(packageName = it) }
+    }
+
+    private fun createDesktopTasksFromPackageNames(
+        packageNames: List<String>
+    ): ArrayList<RunningTaskInfo> {
+        return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) })
+    }
+
+    private fun createDesktopTaskInfo(packageName: String): RunningTaskInfo {
+        return RunningTaskInfo().apply {
+            taskId = nextTaskId++
+            configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+            realActivity = ComponentName(packageName, "TestActivity")
+        }
+    }
+
+    private fun createTestAppInfo(
+        packageName: String = "testPackageName",
+        className: String = "testClassName"
+    ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
+
+    private fun setInDesktopMode(inDesktopMode: Boolean) {
+        whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
+    }
+
+    private fun createSparseArray(itemInfos: List<ItemInfo>): SparseArray<ItemInfo> {
+        val sparseArray = SparseArray<ItemInfo>()
+        itemInfos.forEachIndexed { index, itemInfo -> sparseArray[index] = itemInfo }
+        return sparseArray
+    }
+
+    private companion object {
+        const val HOTSEAT_PACKAGE_1 = "hotseat1"
+        const val HOTSEAT_PACKAGE_2 = "hotseat2"
+        const val RUNNING_APP_PACKAGE_1 = "running1"
+        const val RUNNING_APP_PACKAGE_2 = "running2"
+        const val RUNNING_APP_PACKAGE_3 = "running3"
+        val ALL_APP_PACKAGES =
+            listOf(
+                HOTSEAT_PACKAGE_1,
+                HOTSEAT_PACKAGE_2,
+                RUNNING_APP_PACKAGE_1,
+                RUNNING_APP_PACKAGE_2,
+                RUNNING_APP_PACKAGE_3,
+            )
+    }
+}