Support optional bubble overflow in bubble bar

This is similar to the animations that add / remove a bubble at the
same time -- the overflow is generally added when a bubble is removed.
The overflow is generally removed when a bubble is added (i.e. user
promotes a bubble out of the overflow).

There are a couple of additional cases:
- when bubbles are first added to the bar -- if there were saved
  bubbles in the overflow, the view should be added
- an app could cancel its bubbles / remove its shortcuts and not have
  any in the stack but could have some in the overflow & it could
  become empty without an addition.

Flag: com.android.wm.shell.enable_optional_bubble_overflow
Flag: com.android.wm.shell.enable_bubble_bar
Test: manual - add bubbles to the bubble bar for first time
             => observe there is no overflow
             - dismiss a bubble
             => observe the overflow is added, tap on it, tap on the
                bubble in it
             => observe that bubble is added & the overflow disappears
             - dismiss all the bubbles
             - add a bubble
             => observe the overflow is there & has the previously
                dismissed bubbles
             - cancel all the bubbles that are in the overflow via
               adb
             => observe the overflow is remvoed
Bug: 334175587
Change-Id: I2b6e855e65520b4b2b1fde7757d46f00a468b4a6
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index fbee080..33d8a84 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -138,6 +138,8 @@
         List<RemovedBubble> removedBubbles;
         List<String> bubbleKeysInOrder;
         Point expandedViewDropTargetSize;
+        boolean showOverflow;
+        boolean showOverflowChanged;
 
         // These need to be loaded in the background
         BubbleBarBubble addedBubble;
@@ -156,6 +158,8 @@
             removedBubbles = update.removedBubbles;
             bubbleKeysInOrder = update.bubbleKeysInOrder;
             expandedViewDropTargetSize = update.expandedViewDropTargetSize;
+            showOverflow = update.showOverflow;
+            showOverflowChanged = update.showOverflowChanged;
         }
     }
 
@@ -271,7 +275,13 @@
 
         BubbleBarBubble bubbleToSelect = null;
 
-        if (update.addedBubble != null && update.removedBubbles.size() == 1) {
+        if (Flags.enableOptionalBubbleOverflow()
+                && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null
+                && update.removedBubbles.isEmpty()) {
+            // A bubble was added from the overflow (& now it's empty / not showing)
+            mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
+        } else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
             // we're adding and removing a bubble at the same time. handle this as a single update.
             RemovedBubble removedBubble = update.removedBubbles.get(0);
             BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey());
@@ -285,11 +295,17 @@
                 Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
             }
         } else {
+            boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow()
+                    && update.showOverflowChanged && update.showOverflow;
             if (!update.removedBubbles.isEmpty()) {
                 for (int i = 0; i < update.removedBubbles.size(); i++) {
                     RemovedBubble removedBubble = update.removedBubbles.get(i);
                     BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
-                    if (bubble != null) {
+                    if (bubble != null && overflowNeedsToBeAdded) {
+                        // First removal, show the overflow
+                        overflowNeedsToBeAdded = false;
+                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
+                    } else if (bubble != null) {
                         mBubbleBarViewController.removeBubble(bubble);
                     } else {
                         Log.w(TAG, "trying to remove bubble that doesn't exist: "
@@ -302,6 +318,11 @@
                 mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
                         suppressAnimation);
             }
+            if (Flags.enableOptionalBubbleOverflow()
+                    && update.showOverflowChanged
+                    && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) {
+                mBubbleBarViewController.showOverflow(update.showOverflow);
+            }
         }
 
         // if a bubble was updated upstream, but removed before the update was received, add it back
@@ -333,6 +354,9 @@
                 }
             }
         }
+        if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) {
+            mBubbleBarViewController.showOverflow(true);
+        }
 
         // Adds and removals have happened, update visibility before any other visual changes.
         mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 7d27a90..32ca9f2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -715,11 +715,13 @@
     public void addBubble(BubbleView bubble) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
+        final int index = bubble.isOverflow() ? getChildCount() : 0;
+
         if (isExpanded()) {
             // if we're expanded scale the new bubble in
             bubble.setScaleX(0f);
             bubble.setScaleY(0f);
-            addView(bubble, 0, lp);
+            addView(bubble, index, lp);
             bubble.showDotIfNeeded(/* animate= */ false);
 
             mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
@@ -748,23 +750,33 @@
             };
             mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
         } else {
-            addView(bubble, 0, lp);
+            addView(bubble, index, lp);
         }
     }
 
     /** Add a new bubble and remove an old bubble from the bubble bar. */
-    public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) {
+    public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
+        boolean isOverflowSelected = mSelectedBubbleView.isOverflow();
+        boolean removingOverflow = removedBubble.isOverflow();
+        boolean addingOverflow = addedBubble.isOverflow();
+
         if (!isExpanded()) {
             removeView(removedBubble);
-            addView(addedBubble, 0, lp);
+            int index = addingOverflow ? getChildCount() : 0;
+            addView(addedBubble, index, lp);
             return;
         }
+        int index = addingOverflow ? getChildCount() : 0;
         addedBubble.setScaleX(0f);
         addedBubble.setScaleY(0f);
-        addView(addedBubble, 0, lp);
+        addView(addedBubble, index, lp);
 
+        if (isOverflowSelected && removingOverflow) {
+            // The added bubble will be selected
+            mSelectedBubbleView = addedBubble;
+        }
         int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
         int indexOfBubbleToRemove = indexOfChild(removedBubble);
 
@@ -924,7 +936,7 @@
         final float currentWidth = getWidth();
         final float expandedWidth = expandedWidth();
         final float collapsedWidth = collapsedWidth();
-        int bubbleCount = getChildCount();
+        int childCount = getChildCount();
         float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0);
         float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight();
         // When translating X & Y the scale is ignored, so need to deduct it from the translations
@@ -932,7 +944,7 @@
         final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
         // elevation state is opposite to widthState - when expanded all icons are flat
         float elevationState = (1 - widthState);
-        for (int i = 0; i < bubbleCount; i++) {
+        for (int i = 0; i < childCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
                 // Skip the dragged bubble. Its translation is managed by the drag controller.
@@ -951,9 +963,9 @@
             bv.setTranslationY(ty);
 
             // the position of the bubble when the bar is fully expanded
-            final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
+            final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft);
             // the position of the bubble when the bar is fully collapsed
-            final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft);
+            final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft);
 
             // slowly animate elevation while keeping correct Z ordering
             float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
@@ -981,13 +993,10 @@
                 final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
                 final float targetX = collapsedX + collapsedBarShift;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
-                // If we're fully collapsed, hide all bubbles except for the first 2. If there are
-                // only 2 bubbles, hide the second bubble as well because it's the overflow.
+                // If we're fully collapsed, hide all bubbles except for the first 2, excluding
+                // the overflow.
                 if (widthState == 0) {
-                    if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
-                        bv.setAlpha(0);
-                    } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1
-                            && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) {
+                    if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
                         bv.setAlpha(0);
                     } else {
                         bv.setAlpha(1);
@@ -1043,22 +1052,26 @@
         return translationX - getScaleIconShift();
     }
 
-    private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) {
-        if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
+    private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) {
+        if (bubbleIndex < 0 || bubbleIndex >= childCount) {
             return 0;
         }
         float translationX;
         if (onLeft) {
-            // Shift the first bubble only if there are more bubbles in addition to overflow
-            translationX = mBubbleBarPadding + (
-                    bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
-                            ? mIconOverlapAmount : 0);
+            // Shift the first bubble only if there are more bubbles
+            if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
+                translationX = mIconOverlapAmount;
+            } else {
+                translationX = 0f;
+            }
         } else {
-            translationX = mBubbleBarPadding + (
-                    bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED
-                            ? 0 : mIconOverlapAmount);
+            if (bubbleIndex == 1 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
+                translationX = mIconOverlapAmount;
+            } else {
+                translationX = 0f;
+            }
         }
-        return translationX - getScaleIconShift();
+        return mBubbleBarPadding + translationX - getScaleIconShift();
     }
 
     /**
@@ -1256,15 +1269,20 @@
     }
 
     private float collapsedWidth() {
-        final int childCount = getChildCount();
+        final int bubbleChildCount = getBubbleChildCount();
         final float horizontalPadding = 2 * mBubbleBarPadding;
-        // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
-        // Otherwise just the first bubble should be visible because we don't show the overflow.
-        return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED
+        // If there are more than 2 bubbles, the first 2 should be visible when collapsed,
+        // excluding the overflow.
+        return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED
                 ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding
                 : getScaledIconSize() + horizontalPadding;
     }
 
+    /** Returns the child count excluding the overflow if it's present. */
+    private int getBubbleChildCount() {
+        return hasOverflow() ? getChildCount() - 1 : getChildCount();
+    }
+
     private float getBubbleBarExpandedHeight() {
         return getBubbleBarCollapsedHeight() + mPointerSize;
     }
@@ -1303,8 +1321,8 @@
         return mIsAnimatingNewBubble;
     }
 
-    private boolean hasOverview() {
-        // Overview is always the last bubble
+    private boolean hasOverflow() {
+        // Overflow is always the last bubble
         View lastChild = getChildAt(getChildCount() - 1);
         if (lastChild instanceof BubbleView bubbleView) {
             return bubbleView.getBubble() instanceof BubbleBarOverflow;
@@ -1336,7 +1354,7 @@
         CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : "";
 
         // Don't count overflow if it exists
-        int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0);
+        int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0);
         if (bubbleCount > 1) {
             contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles,
                     contentDesc, bubbleCount - 1);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 590916e..2cdc0ce 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -92,6 +92,8 @@
     private boolean mHiddenForNoBubbles = true;
     private boolean mShouldShowEducation;
 
+    public boolean mOverflowAdded;
+
     private BubbleBarViewAnimator mBubbleBarViewAnimator;
 
     private final TimeSource mTimeSource = System::currentTimeMillis;
@@ -123,7 +125,6 @@
         mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
         mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
-        addOverflow();
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -494,13 +495,44 @@
         }
     }
 
-    /**
-     * Adds the overflow view to the bubble bar.
-     */
-    public void addOverflow() {
-        mBarView.addBubble(mOverflowBubble.getView());
+    /** Whether the overflow view is added to the bubble bar. */
+    public boolean isOverflowAdded() {
+        return mOverflowAdded;
+    }
+
+    /** Shows or hides the overflow view. */
+    public void showOverflow(boolean showOverflow) {
+        if (mOverflowAdded == showOverflow) return;
+        mOverflowAdded = showOverflow;
+        if (mOverflowAdded) {
+            mBarView.addBubble(mOverflowBubble.getView());
+            mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
+            mOverflowBubble.getView().setController(mBubbleViewController);
+        } else {
+            mBarView.removeBubble(mOverflowBubble.getView());
+            mOverflowBubble.getView().setOnClickListener(null);
+            mOverflowBubble.getView().setController(null);
+        }
+    }
+
+    /** Adds the overflow view to the bubble bar while animating a view away. */
+    public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) {
+        if (mOverflowAdded) return;
+        mOverflowAdded = true;
+        mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView());
         mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
         mOverflowBubble.getView().setController(mBubbleViewController);
+        removedBubble.getView().setController(null);
+    }
+
+    /** Removes the overflow view to the bubble bar while animating a view in. */
+    public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) {
+        if (!mOverflowAdded) return;
+        mOverflowAdded = false;
+        mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView());
+        addedBubble.getView().setOnClickListener(mBubbleClickListener);
+        addedBubble.getView().setController(mBubbleViewController);
+        mOverflowBubble.getView().setController(null);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 09da3e0..f0f2872 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -74,6 +74,7 @@
     private boolean mOnLeft = false;
 
     private BubbleBarItem mBubble;
+    private boolean mIsOverflow;
 
     private Bitmap mIcon;
 
@@ -271,12 +272,18 @@
      */
     public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
         mBubble = overflow;
+        mIsOverflow = true;
         mIcon = bitmap;
         updateBubbleIcon();
         mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
         setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description));
     }
 
+    /** Whether this view represents the overflow button. */
+    public boolean isOverflow() {
+        return mIsOverflow;
+    }
+
     /** Returns the bubble being rendered in this view. */
     @Nullable
     public BubbleBarItem getBubble() {