Merge changes from topic "mm_bubbleInfoFlags" into udc-qpr-dev

* changes:
  Show / hide the "update" dot on bubbles in bubble bar
  Handle any image / label changes for bubble updates in bubble bar
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 056fc74..7e2b037 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -33,6 +33,7 @@
 
 import android.annotation.BinderThread;
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -243,17 +244,21 @@
             BUBBLE_STATE_EXECUTOR.execute(() -> {
                 createAndAddOverflowIfNeeded();
                 if (update.addedBubble != null) {
-                    viewUpdate.addedBubble = populateBubble(update.addedBubble, mContext, mBarView);
+                    viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
+                            null /* existingBubble */);
                 }
                 if (update.updatedBubble != null) {
+                    BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
                     viewUpdate.updatedBubble =
-                            populateBubble(update.updatedBubble, mContext, mBarView);
+                            populateBubble(mContext, update.updatedBubble, mBarView,
+                                    existingBubble);
                 }
                 if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
                     List<BubbleBarBubble> currentBubbles = new ArrayList<>();
                     for (int i = 0; i < update.currentBubbleList.size(); i++) {
                         BubbleBarBubble b =
-                                populateBubble(update.currentBubbleList.get(i), mContext, mBarView);
+                                populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
+                                        null /* existingBubble */);
                         currentBubbles.add(b);
                     }
                     viewUpdate.currentBubbles = currentBubbles;
@@ -315,9 +320,11 @@
         mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
 
         if (update.updatedBubble != null) {
-            // TODO: (b/269670235) handle updates:
-            //  (1) if content / icons change -- requires reload & add back in place
-            //  (2) if showing update dot changes -- tell the view to hide / show the dot
+            // Updates mean the dot state may have changed; any other changes were updated in
+            // the populateBubble step.
+            BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
+            // If we're not stashed, we're visible so animate
+            bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
         }
         if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
             // Create the new list
@@ -362,7 +369,13 @@
         if (getSelectedBubbleKey() != null) {
             int[] bubbleBarCoords = mBarView.getLocationOnScreen();
             if (mSelectedBubble instanceof BubbleBarBubble) {
-                // TODO (b/269670235): hide the update dot on the view if needed.
+                // Because we've visited this bubble, we should suppress the notification.
+                // This is updated on WMShell side when we show the bubble, but that update isn't
+                // passed to launcher, instead we apply it directly here.
+                BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
+                info.setFlags(
+                        info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+                mSelectedBubble.getView().updateDotVisibility(true /* animate */);
             }
             mSystemUiProxy.showBubble(getSelectedBubbleKey(),
                     bubbleBarCoords[0], bubbleBarCoords[1]);
@@ -407,7 +420,8 @@
     //
 
     @Nullable
-    private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) {
+    private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
+            @Nullable BubbleBarBubble existingBubble) {
         String appName;
         Bitmap badgeBitmap;
         Bitmap bubbleBitmap;
@@ -476,16 +490,27 @@
         iconPath.transform(matrix);
         dotPath = iconPath;
         dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
-                Color.WHITE, WHITE_SCRIM_ALPHA);
+                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
 
-        LayoutInflater inflater = LayoutInflater.from(context);
-        BubbleView bubbleView = (BubbleView) inflater.inflate(
-                R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
+        if (existingBubble == null) {
+            LayoutInflater inflater = LayoutInflater.from(context);
+            BubbleView bubbleView = (BubbleView) inflater.inflate(
+                    R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
 
-        BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
-                badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
-        bubbleView.setBubble(bubble);
-        return bubble;
+            BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
+                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
+            bubbleView.setBubble(bubble);
+            return bubble;
+        } else {
+            // If we already have a bubble (so it already has an inflated view), update it.
+            existingBubble.setInfo(b);
+            existingBubble.setBadge(badgeBitmap);
+            existingBubble.setIcon(bubbleBitmap);
+            existingBubble.setDotColor(dotColor);
+            existingBubble.setDotPath(dotPath);
+            existingBubble.setAppName(appName);
+            return existingBubble;
+        }
     }
 
     private BubbleBarOverflow createOverflow(Context context) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 582dcc7..43e21f4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -20,18 +20,18 @@
 import com.android.wm.shell.common.bubbles.BubbleInfo
 
 /** An entity in the bubble bar. */
-sealed class BubbleBarItem(open val key: String, open val view: BubbleView)
+sealed class BubbleBarItem(open var key: String, open var view: BubbleView)
 
 /** Contains state info about a bubble in the bubble bar as well as presentation information. */
 data class BubbleBarBubble(
-    val info: BubbleInfo,
-    override val view: BubbleView,
-    val badge: Bitmap,
-    val icon: Bitmap,
-    val dotColor: Int,
-    val dotPath: Path,
-    val appName: String
+    var info: BubbleInfo,
+    override var view: BubbleView,
+    var badge: Bitmap,
+    var icon: Bitmap,
+    var dotColor: Int,
+    var dotPath: Path,
+    var appName: String
 ) : BubbleBarItem(info.key, view)
 
 /** Represents the overflow bubble in the bubble bar. */
-data class BubbleBarOverflow(override val view: BubbleView) : BubbleBarItem("Overflow", view)
+data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 563ba02..cf52a5e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -234,6 +234,7 @@
         final float collapsedWidth = collapsedWidth();
         int bubbleCount = getChildCount();
         final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
+        final boolean animate = getVisibility() == VISIBLE;
         for (int i = 0; i < bubbleCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             bv.setTranslationY(ty);
@@ -251,16 +252,14 @@
                 if (widthState == 1f) {
                     bv.setZ(0);
                 }
-                bv.showBadge();
+                // When we're expanded, we're not stacked so we're not behind the stack
+                bv.setBehindStack(false, animate);
             } else {
                 final float targetX = currentWidth - collapsedWidth + collapsedX;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                 bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
-                if (i > 0) {
-                    bv.hideBadge();
-                } else {
-                    bv.showBadge();
-                }
+                // If we're not the first bubble we're behind the stack
+                bv.setBehindStack(i > 0, animate);
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 92b76a6..12cb8c5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -18,7 +18,9 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Outline;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -28,10 +30,13 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
+import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
+import com.android.wm.shell.animation.Interpolators;
+
+import java.util.EnumSet;
 
 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
-// TODO: (b/269670235) currently this doesn't show the 'update dot'
 
 /**
  * View that displays a bubble icon, along with an app badge on either the left or
@@ -39,14 +44,42 @@
  */
 public class BubbleView extends ConstraintLayout {
 
-    // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
     public static final int DEFAULT_PATH_SIZE = 100;
 
+    /**
+     * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
+     * another. If any of these flags are set, the dot will not be shown.
+     * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
+     */
+    enum SuppressionFlag {
+        // TODO: (b/277815200) implement flyout
+        // Suppressed because the flyout is visible - it will morph into the dot via animation.
+        FLYOUT_VISIBLE,
+        // Suppressed because this bubble is behind others in the collapsed stack.
+        BEHIND_STACK,
+    }
+
+    private final EnumSet<SuppressionFlag> mSuppressionFlags =
+            EnumSet.noneOf(SuppressionFlag.class);
+
     private final ImageView mBubbleIcon;
     private final ImageView mAppIcon;
     private final int mBubbleSize;
 
+    private DotRenderer mDotRenderer;
+    private DotRenderer.DrawParams mDrawParams;
+    private int mDotColor;
+    private Rect mTempBounds = new Rect();
+
+    // Whether the dot is animating
+    private boolean mDotIsAnimating;
+    // What scale value the dot is animating to
+    private float mAnimatingToDotScale;
+    // The current scale value of the dot
+    private float mDotScale;
+
     // TODO: (b/273310265) handle RTL
+    // Whether the bubbles are positioned on the left or right side of the screen
     private boolean mOnLeft = false;
 
     private BubbleBarItem mBubble;
@@ -75,6 +108,8 @@
         mBubbleIcon = findViewById(R.id.icon_view);
         mAppIcon = findViewById(R.id.app_icon_view);
 
+        mDrawParams = new DotRenderer.DrawParams();
+
         setFocusable(true);
         setClickable(true);
         setOutlineProvider(new ViewOutlineProvider() {
@@ -91,17 +126,43 @@
         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
     }
 
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (!shouldDrawDot()) {
+            return;
+        }
+
+        getDrawingRect(mTempBounds);
+
+        mDrawParams.dotColor = mDotColor;
+        mDrawParams.iconBounds = mTempBounds;
+        mDrawParams.leftAlign = mOnLeft;
+        mDrawParams.scale = mDotScale;
+
+        mDotRenderer.draw(canvas, mDrawParams);
+    }
+
     /** Sets the bubble being rendered in this view. */
     void setBubble(BubbleBarBubble bubble) {
         mBubble = bubble;
         mBubbleIcon.setImageBitmap(bubble.getIcon());
         mAppIcon.setImageBitmap(bubble.getBadge());
+        mDotColor = bubble.getDotColor();
+        mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE);
     }
 
+    /**
+     * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
+     * but does not represent app content, instead it shows recent bubbles that couldn't fit into
+     * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
+     * come from an app.
+     */
     void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
         mBubble = overflow;
         mBubbleIcon.setImageBitmap(bitmap);
-        hideBadge();
+        mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
     }
 
     /** Returns the bubble being rendered in this view. */
@@ -110,38 +171,102 @@
         return mBubble;
     }
 
-    /** Shows the app badge on this bubble. */
-    void showBadge() {
+    void updateDotVisibility(boolean animate) {
+        final float targetScale = shouldDrawDot() ? 1f : 0f;
+        if (animate) {
+            animateDotScale();
+        } else {
+            mDotScale = targetScale;
+            mAnimatingToDotScale = targetScale;
+            invalidate();
+        }
+    }
+
+    void updateBadgeVisibility() {
         if (mBubble instanceof BubbleBarOverflow) {
             // The overflow bubble does not have a badge, so just bail.
             return;
         }
         BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
-
         Bitmap appBadgeBitmap = bubble.getBadge();
-        if (appBadgeBitmap == null) {
-            mAppIcon.setVisibility(GONE);
+        int translationX = mOnLeft
+                ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
+                : 0;
+        mAppIcon.setTranslationX(translationX);
+        mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
+    }
+
+    /** Sets whether this bubble is in the stack & not the first bubble. **/
+    void setBehindStack(boolean behindStack, boolean animate) {
+        if (behindStack) {
+            mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
+        } else {
+            mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
+        }
+        updateDotVisibility(animate);
+        updateBadgeVisibility();
+    }
+
+    /** Whether this bubble is in the stack & not the first bubble. **/
+    boolean isBehindStack() {
+        return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
+    }
+
+    /** Whether the dot indicating unseen content in a bubble should be shown. */
+    private boolean shouldDrawDot() {
+        boolean bubbleHasUnseenContent = mBubble != null
+                && mBubble instanceof BubbleBarBubble
+                && mSuppressionFlags.isEmpty()
+                && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
+
+        // Always render the dot if it's animating, since it could be animating out. Otherwise, show
+        // it if the bubble wants to show it, and we aren't suppressing it.
+        return bubbleHasUnseenContent || mDotIsAnimating;
+    }
+
+    /** How big the dot should be, fraction from 0 to 1. */
+    private void setDotScale(float fraction) {
+        mDotScale = fraction;
+        invalidate();
+    }
+
+    /**
+     * Animates the dot to the given scale.
+     */
+    private void animateDotScale() {
+        float toScale = shouldDrawDot() ? 1f : 0f;
+        mDotIsAnimating = true;
+
+        // Don't restart the animation if we're already animating to the given value.
+        if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
+            mDotIsAnimating = false;
             return;
         }
 
-        int translationX;
-        if (mOnLeft) {
-            translationX = -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
-        } else {
-            translationX = 0;
-        }
+        mAnimatingToDotScale = toScale;
 
-        mAppIcon.setTranslationX(translationX);
-        mAppIcon.setVisibility(VISIBLE);
+        final boolean showDot = toScale > 0f;
+
+        // Do NOT wait until after animation ends to setShowDot
+        // to avoid overriding more recent showDot states.
+        clearAnimation();
+        animate()
+                .setDuration(200)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .setUpdateListener((valueAnimator) -> {
+                    float fraction = valueAnimator.getAnimatedFraction();
+                    fraction = showDot ? fraction : 1f - fraction;
+                    setDotScale(fraction);
+                }).withEndAction(() -> {
+                    setDotScale(showDot ? 1f : 0f);
+                    mDotIsAnimating = false;
+                }).start();
     }
 
-    /** Hides the app badge on this bubble. */
-    void hideBadge() {
-        mAppIcon.setVisibility(GONE);
-    }
 
     @Override
     public String toString() {
-        return "BubbleView{" + mBubble + "}";
+        String toString = mBubble != null ? mBubble.getKey() : "null";
+        return "BubbleView{" + toString + "}";
     }
 }