[Desktop Mode] Show indicators under Taskbar app icons for running apps

In Desktop Mode we show running Desktop apps in the Taskbar. With this
change we also show an indicator under the app icon for each such
running app.

Bug: 332504528
Test: manual
Test: DesktopTaskbarRunningAppsControllerTest
Flag: ACONFIG com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps DEVELOPMENT
Change-Id: If0906dab8ad0bd8a78d93a4e99db47550e763bed
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 1b5b0ee..14a916f 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -31,6 +31,7 @@
     <color name="taskbar_nav_icon_dark_color_on_home">#99000000</color>
     <color name="taskbar_stashed_handle_light_color">#EBffffff</color>
     <color name="taskbar_stashed_handle_dark_color">#99000000</color>
+    <color name="taskbar_running_app_indicator_color">#646464</color>
 
     <!-- Floating rotation button -->
     <color name="floating_rotation_button_light_color">#ffffff</color>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index dbf075c..b862d7c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -353,6 +353,9 @@
     <dimen name="taskbar_back_button_suw_start_margin">48dp</dimen>
     <dimen name="taskbar_back_button_suw_bottom_margin">1dp</dimen>
     <dimen name="taskbar_back_button_suw_height">72dp</dimen>
+    <dimen name="taskbar_running_app_indicator_height">4dp</dimen>
+    <dimen name="taskbar_running_app_indicator_width">14dp</dimen>
+    <dimen name="taskbar_running_app_indicator_top_margin">2dp</dimen>
 
     <!-- Transient taskbar -->
     <dimen name="transient_taskbar_padding">12dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
index f665e21..06d25a2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
@@ -80,6 +80,13 @@
         return newHotseatItemInfos.toTypedArray()
     }
 
+    override fun getRunningApps(): Set<String> {
+        if (!isInDesktopMode) {
+            return emptySet()
+        }
+        return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet()
+    }
+
     @VisibleForTesting
     public override fun updateRunningApps(hotseatItems: SparseArray<ItemInfo>?) {
         if (!isInDesktopMode) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 6c84f80..be87cfd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -47,6 +47,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -233,15 +234,23 @@
         }
         hotseatItemInfos = mControllers.taskbarRecentAppsController
                 .updateHotseatItemInfos(hotseatItemInfos);
+        Set<String> runningPackages = mControllers.taskbarRecentAppsController.getRunningApps();
 
         if (mDeferUpdatesForSUW) {
             ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
-            mDeferredUpdates = () -> mContainer.updateHotseatItems(finalHotseatItemInfos);
+            mDeferredUpdates = () ->
+                    commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages);
         } else {
-            mContainer.updateHotseatItems(hotseatItemInfos);
+            commitHotseatItemUpdates(hotseatItemInfos, runningPackages);
         }
     }
 
+    private void commitHotseatItemUpdates(
+            ItemInfo[] hotseatItemInfos, Set<String> runningPackages) {
+        mContainer.updateHotseatItems(hotseatItemInfos);
+        mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages);
+    }
+
     /**
      * This is used to defer UI updates after SUW builds the unstash animation.
      * @param defer if true, defers updates to the UI
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java
index 8445cff..9b84f1b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.taskbar;
 
+import static java.util.Collections.emptySet;
+
 import android.util.SparseArray;
 
 import androidx.annotation.CallSuper;
@@ -22,6 +24,8 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 
+import java.util.Set;
+
 /**
  * Base class for providing recent apps functionality
  */
@@ -43,7 +47,8 @@
     }
 
     /** Stores the current {@link AppInfo} instances, no-op except in desktop environment. */
-    protected void setApps(AppInfo[] apps) { }
+    protected void setApps(AppInfo[] apps) {
+    }
 
     /**
      * Indicates whether recent apps functionality is enabled, should return false except in
@@ -59,5 +64,11 @@
     }
 
     /** Called to update the list of currently running apps, no-op except in desktop environment. */
-    protected void updateRunningApps(SparseArray<ItemInfo> hotseatItems) { }
+    protected void updateRunningApps(SparseArray<ItemInfo> hotseatItems) {
+    }
+
+    /** Returns the currently running apps, or an empty Set if outside of Desktop environment. */
+    public Set<String> getRunningApps() {
+        return emptySet();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 5d0eac3..e0b446e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.views.IconButtonView;
 
 import java.io.PrintWriter;
+import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -507,6 +508,15 @@
         return mTaskbarView.getTaskbarDividerView();
     }
 
+    /** Updates which icons are marked as running given the Set of currently running packages. */
+    public void updateIconViewsRunningStates(Set<String> runningPackages) {
+        for (View iconView : getIconViews()) {
+            if (iconView instanceof BubbleTextView btv) {
+                btv.updateRunningState(runningPackages.contains(btv.getTargetPackageName()));
+            }
+        }
+    }
+
     /**
      * Defers any updates to the UI for the setup wizard animation.
      */
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
index 93eefe2..2cfcf38 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
@@ -154,6 +154,29 @@
         assertThat(newHotseatItems?.map { it.targetPackage }).isEqualTo(expectedPackages)
     }
 
+    @Test
+    fun getRunningApps_notInDesktopMode_returnsEmptySet() {
+        setInDesktopMode(false)
+        val runningTasks =
+            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
+        taskbarRunningAppsController.updateRunningApps(createSparseArray(emptyList()))
+
+        assertThat(taskbarRunningAppsController.runningApps).isEqualTo(emptySet<String>())
+    }
+
+    @Test
+    fun getRunningApps_inDesktopMode_returnsRunningApps() {
+        setInDesktopMode(true)
+        val runningTasks =
+            createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+        whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
+        taskbarRunningAppsController.updateRunningApps(createSparseArray(emptyList()))
+
+        assertThat(taskbarRunningAppsController.runningApps)
+            .isEqualTo(setOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+    }
+
     private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
         return packageNames.map { createTestAppInfo(packageName = it) }
     }
diff --git a/res/values/colors.xml b/res/values/colors.xml
index a620eb0..dfe40fc 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -90,6 +90,8 @@
     <color name="drop_target_hover_button_color_light">#D3E3FD</color>
     <color name="drop_target_hover_button_color_dark">#0842A0</color>
 
+    <color name="taskbar_running_app_indicator_color">#000000</color>
+
     <color name="preload_icon_accent_color_light">#00668B</color>
     <color name="preload_icon_background_color_light">#B5CAD7</color>
     <color name="preload_icon_accent_color_dark">#4BB6E8</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 31def04..1bf59e8 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -394,6 +394,9 @@
     <dimen name="min_hotseat_icon_space">18dp</dimen>
     <dimen name="max_hotseat_icon_space">50dp</dimen>
     <dimen name="min_hotseat_qsb_width">0dp</dimen>
+    <dimen name="taskbar_running_app_indicator_height">0dp</dimen>
+    <dimen name="taskbar_running_app_indicator_width">0dp</dimen>
+    <dimen name="taskbar_running_app_indicator_top_margin">0dp</dimen>
 
     <!-- Transient taskbar (placeholders to compile in Launcher3 without Quickstep) -->
     <dimen name="transient_taskbar_padding">0dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 83236d1..7f316e2 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -46,6 +46,7 @@
 import android.text.TextUtils.TruncateAt;
 import android.util.AttributeSet;
 import android.util.Property;
+import android.util.Size;
 import android.util.TypedValue;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -182,6 +183,13 @@
     private Animator mDotScaleAnim;
     private boolean mForceHideDot;
 
+    // These fields, related to showing running apps, are only used for Taskbar.
+    private final Size mRunningAppIndicatorSize;
+    private final int mRunningAppIndicatorTopMargin;
+    private final Paint mRunningAppIndicatorPaint;
+    private final Rect mRunningAppIconBounds = new Rect();
+    private boolean mIsRunning;
+
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mStayPressed;
     @ViewDebug.ExportedProperty(category = "launcher")
@@ -248,6 +256,16 @@
                 defaultIconSize);
         a.recycle();
 
+        mRunningAppIndicatorSize = new Size(
+                getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width),
+                getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height));
+        mRunningAppIndicatorTopMargin =
+                getResources().getDimensionPixelSize(
+                        R.dimen.taskbar_running_app_indicator_top_margin);
+        mRunningAppIndicatorPaint = new Paint();
+        mRunningAppIndicatorPaint.setColor(getResources().getColor(
+                R.color.taskbar_running_app_indicator_color, context.getTheme()));
+
         mLongPressHelper = new CheckLongPressHelper(this);
 
         mDotParams = new DotRenderer.DrawParams();
@@ -394,6 +412,12 @@
         setDownloadStateContentDescription(info, info.getProgressLevel());
     }
 
+    /** Updates whether the app this view represents is currently running. */
+    @UiThread
+    public void updateRunningState(boolean isRunning) {
+        mIsRunning = isRunning;
+    }
+
     protected void setItemInfo(ItemInfoWithIcon itemInfo) {
         setTag(itemInfo);
     }
@@ -620,6 +644,7 @@
     public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         drawDotIfNecessary(canvas);
+        drawRunningAppIndicatorIfNecessary(canvas);
     }
 
     /**
@@ -640,6 +665,22 @@
         }
     }
 
+    /** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
+    protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
+        if (!mIsRunning || mDisplay != DISPLAY_TASKBAR) {
+            return;
+        }
+        getIconBounds(mRunningAppIconBounds);
+        // TODO(b/333872717): update color, shape, and size of indicator
+        int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
+        canvas.drawRect(
+                mRunningAppIconBounds.centerX() - mRunningAppIndicatorSize.getWidth() / 2,
+                indicatorTop,
+                mRunningAppIconBounds.centerX() + mRunningAppIndicatorSize.getWidth() / 2,
+                indicatorTop + mRunningAppIndicatorSize.getHeight(),
+                mRunningAppIndicatorPaint);
+    }
+
     @Override
     public void setForceHideDot(boolean forceHideDot) {
         if (mForceHideDot == forceHideDot) {
@@ -1230,4 +1271,13 @@
     public boolean canShowLongPressPopup() {
         return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag());
     }
+
+    /** Returns the package name of the app this icon represents. */
+    public String getTargetPackageName() {
+        Object tag = getTag();
+        if (tag instanceof ItemInfo itemInfo) {
+            return itemInfo.getTargetPackage();
+        }
+        return null;
+    }
 }
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index a309e6e..bc66a33 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -77,6 +77,7 @@
         canvas.restore();
 
         drawDotIfNecessary(canvas);
+        drawRunningAppIndicatorIfNecessary(canvas);
     }
 
     public static class ShadowInfo {