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