Bubble overflow - core functionality
BubbleStackView
- create show-overflow-button
- create expanded view to show overflow activity
BubbleOverflowActivity
- add adapter for recycler view of BadgedImageViews
- select bubble to promote it out of overflow, to first bubble in row
BubbleExpandedView
- create overflow intent
- add null bubble checks for overflow expanded view
BubbleData
- new list: overflow bubbles
- only repack bubbles in row (leave overflow order alone)
- update sortKey() to account for last access in addition to last
update. When users select a bubble to promote it out of overflow, it
counts as one access (instead of update); if sortKey does not check last
accessed time, the order is lost in the next repacking and the bubble goes
back to overflow.
- remove oldest bubbles if overflow bubble count > 16
BubbleController
- add callback that updates overflow activity when data changes
- allow overflow activity to set callback
- add/remove bubbles from bubble row ui
BadgedImageView
- set update() param to bubble only
- save other params in Bubble.java so that overflow can update
BadgedImageView with just a bubble
Bug: 138116789
Fixes: 148232991
Fixes: 148232992
Test: (manual) add 5+ bubbles: show-overflow-button shows in bubble row
Test: (manual) tap show-overflow-button: overflow shows aged out bubbles
Test: (manual) remove bubbles, count <= 5: overflow button hides
Test: (manual) tap bubble in overflow: bubble promoted to left of top row
Test: atest SystemUITests
Change-Id: I020ee4c9e16b236043c5cc244e610725e86bcc37
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
index dc24996..601bae2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
@@ -89,12 +89,12 @@
/**
* Updates the view with provided info.
*/
- public void update(Bubble bubble, Bitmap bubbleImage, int dotColor, Path dotPath) {
+ public void update(Bubble bubble) {
mBubble = bubble;
- setImageBitmap(bubbleImage);
+ setImageBitmap(bubble.getBadgedImage());
setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
- mDotColor = dotColor;
- drawDot(dotPath);
+ mDotColor = bubble.getDotColor();
+ drawDot(bubble.getDotPath());
animateDot();
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 2d9775d..ccce85c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -31,6 +31,8 @@
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -93,6 +95,9 @@
}
private FlyoutMessage mFlyoutMessage;
+ private Bitmap mBadgedImage;
+ private int mDotColor;
+ private Path mDotPath;
public static String groupId(NotificationEntry entry) {
UserHandle user = entry.getSbn().getUser();
@@ -124,6 +129,18 @@
return mEntry.getSbn().getPackageName();
}
+ public Bitmap getBadgedImage() {
+ return mBadgedImage;
+ }
+
+ public int getDotColor() {
+ return mDotColor;
+ }
+
+ public Path getDotPath() {
+ return mDotPath;
+ }
+
@Nullable
public String getAppName() {
return mAppName;
@@ -205,8 +222,12 @@
mAppName = info.appName;
mFlyoutMessage = info.flyoutMessage;
+ mBadgedImage = info.badgedBubbleImage;
+ mDotColor = info.dotColor;
+ mDotPath = info.dotPath;
+
mExpandedView.update(this);
- mIconView.update(this, info.badgedBubbleImage, info.dotColor, info.dotPath);
+ mIconView.update(this);
}
/**
@@ -262,6 +283,13 @@
}
/**
+ * Should be invoked whenever a Bubble is promoted from overflow.
+ */
+ void markUpdatedAt(long lastAccessedMillis) {
+ mLastUpdated = lastAccessedMillis;
+ }
+
+ /**
* Whether this notification should be shown in the shade when it is also displayed as a
* bubble.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index e642d4e..8c9946f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -103,6 +103,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -151,6 +152,7 @@
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
private BubbleIconFactory mBubbleIconFactory;
+ private int mMaxBubbles;
// Tracks the id of the current (foreground) user.
private int mCurrentUserId;
@@ -171,6 +173,8 @@
private StatusBarStateListener mStatusBarStateListener;
private final ScreenshotHelper mScreenshotHelper;
+ // Callback that updates BubbleOverflowActivity on data change.
+ @Nullable private Runnable mOverflowCallback = null;
private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
private IStatusBarService mBarService;
@@ -301,6 +305,7 @@
mBubbleData = data;
mBubbleData.setListener(mBubbleDataListener);
+ mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
mNotificationEntryManager = entryManager;
mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
@@ -370,6 +375,18 @@
mInflateSynchronously = inflateSynchronously;
}
+ void setOverflowCallback(Runnable updateOverflow) {
+ mOverflowCallback = updateOverflow;
+ }
+
+ /**
+ * @return Bubbles for updating overflow.
+ */
+ List<Bubble> getOverflowBubbles() {
+ return mBubbleData.getOverflowBubbles();
+ }
+
+
/**
* BubbleStackView is lazily created by this method the first time a Bubble is added. This
* method initializes the stack view and adds it to the StatusBar just above the scrim.
@@ -537,6 +554,10 @@
mBubbleData.setSelectedBubble(bubble);
}
+ void promoteBubbleFromOverflow(Bubble bubble) {
+ mBubbleData.promoteBubbleFromOverflow(bubble);
+ }
+
/**
* Request the stack expand if needed, then select the specified Bubble as current.
*
@@ -817,6 +838,11 @@
@Override
public void applyUpdate(BubbleData.Update update) {
+ // Update bubbles in overflow.
+ if (mOverflowCallback != null) {
+ mOverflowCallback.run();
+ }
+
if (update.addedBubble != null) {
mStackView.addBubble(update.addedBubble);
}
@@ -890,6 +916,8 @@
mStackView.updateBubble(update.updatedBubble);
}
+ // At this point, the correct bubbles are inflated in the stack.
+ // Make sure the order in bubble data is reflected in bubble row.
if (update.orderChanged) {
mStackView.updateBubbleOrder(update.bubbles);
}
@@ -912,15 +940,18 @@
updateStack();
if (DEBUG_BUBBLE_CONTROLLER) {
- Log.d(TAG, "[BubbleData]");
+ Log.d(TAG, "\n[BubbleData] bubbles:");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getBubbles(),
mBubbleData.getSelectedBubble()));
if (mStackView != null) {
- Log.d(TAG, "[BubbleStackView]");
+ Log.d(TAG, "\n[BubbleStackView]");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(mStackView.getBubblesOnScreen(),
mStackView.getExpandedBubble()));
}
+ Log.d(TAG, "\n[BubbleData] overflow:");
+ Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getOverflowBubbles(),
+ null));
}
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index cc0824e..8b687e7 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -78,9 +78,11 @@
// A read-only view of the bubbles list, changes there will be reflected here.
final List<Bubble> bubbles;
+ final List<Bubble> overflowBubbles;
- private Update(List<Bubble> bubbleOrder) {
- bubbles = Collections.unmodifiableList(bubbleOrder);
+ private Update(List<Bubble> row, List<Bubble> overflow) {
+ bubbles = Collections.unmodifiableList(row);
+ overflowBubbles = Collections.unmodifiableList(overflow);
}
boolean anythingChanged() {
@@ -113,11 +115,14 @@
private final Context mContext;
/** Bubbles that are actively in the stack. */
private final List<Bubble> mBubbles;
+ /** Bubbles that aged out to overflow. */
+ private final List<Bubble> mOverflowBubbles;
/** Bubbles that are being loaded but haven't been added to the stack just yet. */
private final List<Bubble> mPendingBubbles;
private Bubble mSelectedBubble;
private boolean mExpanded;
private final int mMaxBubbles;
+ private final int mMaxOverflowBubbles;
// State tracked during an operation -- keeps track of what listener events to dispatch.
private Update mStateChange;
@@ -146,9 +151,11 @@
public BubbleData(Context context) {
mContext = context;
mBubbles = new ArrayList<>();
+ mOverflowBubbles = new ArrayList<>();
mPendingBubbles = new ArrayList<>();
- mStateChange = new Update(mBubbles);
+ mStateChange = new Update(mBubbles, mOverflowBubbles);
mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
+ mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
}
public boolean hasBubbles() {
@@ -184,6 +191,19 @@
dispatchPendingChanges();
}
+ public void promoteBubbleFromOverflow(Bubble bubble) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "promoteBubbleFromOverflow: " + bubble);
+ }
+ mOverflowBubbles.remove(bubble);
+ doAdd(bubble);
+ setSelectedBubbleInternal(bubble);
+ // Preserve new order for next repack, which sorts by last updated time.
+ bubble.markUpdatedAt(mTimeSource.currentTimeMillis());
+ trim();
+ dispatchPendingChanges();
+ }
+
/**
* Constructs a new bubble or returns an existing one. Does not add new bubbles to
* bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
@@ -343,6 +363,7 @@
mStateChange.orderChanged = true;
}
mStateChange.addedBubble = bubble;
+
if (!isExpanded()) {
mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
// Top bubble becomes selected.
@@ -407,6 +428,17 @@
mStateChange.orderChanged |= repackAll();
}
+ if (reason == BubbleController.DISMISS_AGED) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "overflowing bubble: " + bubbleToRemove);
+ }
+ mOverflowBubbles.add(0, bubbleToRemove);
+ if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
+ // Remove oldest bubble.
+ mOverflowBubbles.remove(mOverflowBubbles.size() - 1);
+ }
+ }
+
// Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
// Move selection to the new bubble at the same position.
@@ -454,7 +486,7 @@
if (mListener != null && mStateChange.anythingChanged()) {
mListener.applyUpdate(mStateChange);
}
- mStateChange = new Update(mBubbles);
+ mStateChange = new Update(mBubbles, mOverflowBubbles);
}
/**
@@ -689,12 +721,19 @@
}
/**
- * The set of bubbles.
+ * The set of bubbles in row.
*/
@VisibleForTesting(visibility = PRIVATE)
public List<Bubble> getBubbles() {
return Collections.unmodifiableList(mBubbles);
}
+ /**
+ * The set of bubbles in overflow.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public List<Bubble> getOverflowBubbles() {
+ return Collections.unmodifiableList(mOverflowBubbles);
+ }
@VisibleForTesting(visibility = PRIVATE)
Bubble getBubbleWithKey(String key) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 48ce4e9..cf8b2be 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -24,6 +24,7 @@
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.ActivityView;
@@ -83,7 +84,7 @@
private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING;
private int mTaskId = -1;
- private PendingIntent mBubbleIntent;
+ private PendingIntent mPendingIntent;
private boolean mKeyboardVisible;
private boolean mNeedsNewHeight;
@@ -98,7 +99,9 @@
private int[] mTempLoc = new int[2];
private int mExpandedViewTouchSlop;
- private Bubble mBubble;
+ @Nullable private Bubble mBubble;
+
+ private boolean mIsOverflow;
private BubbleController mBubbleController = Dependency.get(BubbleController.class);
private WindowManager mWindowManager;
@@ -125,7 +128,7 @@
+ "bubble=" + getBubbleKey());
}
try {
- if (mBubble.usingShortcutInfo()) {
+ if (!mIsOverflow && mBubble.usingShortcutInfo()) {
mActivityView.startShortcutActivity(mBubble.getShortcutInfo(),
options, null /* sourceBounds */);
} else {
@@ -133,7 +136,7 @@
// Apply flags to make behaviour match documentLaunchMode=always.
fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
- mActivityView.startActivity(mBubbleIntent, fillInIntent, options);
+ mActivityView.startActivity(mPendingIntent, fillInIntent, options);
}
} catch (RuntimeException e) {
// If there's a runtime exception here then there's something
@@ -141,7 +144,7 @@
// the bubble again so we'll just remove it.
Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
+ ", " + e.getMessage() + "; removing bubble");
- mBubbleController.removeBubble(mBubble.getKey(),
+ mBubbleController.removeBubble(getBubbleKey(),
BubbleController.DISMISS_INVALID_INTENT);
}
});
@@ -241,6 +244,7 @@
mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
true /* singleTaskInstance */);
+
// Set ActivityView's alpha value as zero, since there is no view content to be shown.
setContentVisibility(false);
addView(mActivityView);
@@ -342,6 +346,15 @@
mStackView = stackView;
}
+ public void setOverflow(boolean overflow) {
+ mIsOverflow = overflow;
+
+ Intent target = new Intent(mContext, BubbleOverflowActivity.class);
+ mPendingIntent = PendingIntent.getActivity(mContext, /* requestCode */ 0,
+ target, PendingIntent.FLAG_UPDATE_CURRENT);
+ mSettingsIcon.setVisibility(GONE);
+ }
+
/**
* Sets the bubble used to populate this view.
*/
@@ -350,14 +363,14 @@
Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null"));
}
boolean isNew = mBubble == null;
- if (isNew || bubble.getKey().equals(mBubble.getKey())) {
+ if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) {
mBubble = bubble;
mSettingsIcon.setContentDescription(getResources().getString(
R.string.bubbles_settings_button_description, bubble.getAppName()));
if (isNew) {
- mBubbleIntent = mBubble.getBubbleIntent();
- if (mBubbleIntent != null || mBubble.getShortcutInfo() != null) {
+ mPendingIntent = mBubble.getBubbleIntent();
+ if (mPendingIntent != null || mBubble.getShortcutInfo() != null) {
setContentVisibility(false);
mActivityView.setVisibility(VISIBLE);
}
@@ -393,12 +406,16 @@
return true;
}
+ // TODO(138116789) Fix overflow height.
void updateHeight() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "updateHeight: bubble=" + getBubbleKey());
}
if (usingActivityView()) {
- float desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight);
+ float desiredHeight = mMinHeight;
+ if (!mIsOverflow) {
+ desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight);
+ }
float height = Math.min(desiredHeight, getMaxExpandedHeight());
height = Math.max(height, mMinHeight);
LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
@@ -423,8 +440,12 @@
int bottomInset = getRootWindowInsets() != null
? getRootWindowInsets().getStableInsetBottom()
: 0;
- return mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
+ int mh = mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
- mPointerMargin - bottomInset;
+ Log.i(TAG, "max exp height: " + mh);
+// return mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
+// - mPointerMargin - bottomInset;
+ return mh;
}
/**
@@ -543,7 +564,7 @@
}
private boolean usingActivityView() {
- return (mBubbleIntent != null || mBubble.getShortcutInfo() != null)
+ return (mPendingIntent != null || mBubble.getShortcutInfo() != null)
&& mActivityView != null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
index 4252f72..006de84 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -76,6 +76,9 @@
private static final String ALLOW_BUBBLE_MENU = "allow_bubble_screenshot_menu";
private static final boolean ALLOW_BUBBLE_MENU_DEFAULT = false;
+ private static final String ALLOW_BUBBLE_OVERFLOW = "allow_bubble_overflow";
+ private static final boolean ALLOW_BUBBLE_OVERFLOW_DEFAULT = false;
+
/**
* When true, if a notification has the information necessary to bubble (i.e. valid
* contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata}
@@ -141,6 +144,16 @@
}
/**
+ * When true, show a menu when a bubble is long-pressed, which will allow the user to take
+ * actions on that bubble.
+ */
+ static boolean allowBubbleOverflow(Context context) {
+ return Settings.Secure.getInt(context.getContentResolver(),
+ ALLOW_BUBBLE_OVERFLOW,
+ ALLOW_BUBBLE_OVERFLOW_DEFAULT ? 1 : 0) != 0;
+ }
+
+ /**
* If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds
* {@link android.app.Notification.BubbleMetadata} to the notification entry as long as
* the notification has necessary info for BubbleMetadata.
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
index d99607f..bea55c8 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
@@ -1,15 +1,42 @@
+/*
+ * Copyright (C) 2020 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.systemui.bubbles;
+import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_OVERFLOW;
+import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
+import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+
import android.app.Activity;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.systemui.R;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
import javax.inject.Inject;
/**
@@ -17,9 +44,13 @@
* Must be public to be accessible to androidx...AppComponentFactory
*/
public class BubbleOverflowActivity extends Activity {
- private RecyclerView mRecyclerView;
- private int mMaxBubbles;
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES;
+
private BubbleController mBubbleController;
+ private BubbleOverflowAdapter mAdapter;
+ private RecyclerView mRecyclerView;
+ private List<Bubble> mOverflowBubbles = new ArrayList<>();
+ private int mMaxBubbles;
@Inject
public BubbleOverflowActivity(BubbleController controller) {
@@ -35,17 +66,42 @@
mMaxBubbles = getResources().getInteger(R.integer.bubbles_max_rendered);
mRecyclerView = findViewById(R.id.bubble_overflow_recycler);
mRecyclerView.setLayoutManager(
- new GridLayoutManager(getApplicationContext(), /* numberOfColumns */ mMaxBubbles));
+ new GridLayoutManager(getApplicationContext(),
+ getResources().getInteger(R.integer.bubbles_overflow_columns)));
+
+ mAdapter = new BubbleOverflowAdapter(mOverflowBubbles,
+ mBubbleController::promoteBubbleFromOverflow);
+ mRecyclerView.setAdapter(mAdapter);
+
+ updateData(mBubbleController.getOverflowBubbles());
+ mBubbleController.setOverflowCallback(() -> {
+ updateData(mBubbleController.getOverflowBubbles());
+ });
}
void setBackgroundColor() {
final TypedArray ta = getApplicationContext().obtainStyledAttributes(
- new int[] {android.R.attr.colorBackgroundFloating});
+ new int[]{android.R.attr.colorBackgroundFloating});
int bgColor = ta.getColor(0, Color.WHITE);
ta.recycle();
findViewById(android.R.id.content).setBackgroundColor(bgColor);
}
+ void updateData(List<Bubble> bubbles) {
+ mOverflowBubbles.clear();
+ if (bubbles.size() > mMaxBubbles) {
+ mOverflowBubbles.addAll(bubbles.subList(mMaxBubbles, bubbles.size()));
+ } else {
+ mOverflowBubbles.addAll(bubbles);
+ }
+ mAdapter.notifyDataSetChanged();
+
+ if (DEBUG_OVERFLOW) {
+ Log.d(TAG, "Updated overflow bubbles:\n" + BubbleDebugConfig.formatBubblesString(
+ mOverflowBubbles, /*selected*/ null));
+ }
+ }
+
@Override
public void onStart() {
super.onStart();
@@ -75,3 +131,48 @@
super.onDestroy();
}
}
+
+class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.ViewHolder> {
+ private Consumer<Bubble> mPromoteBubbleFromOverflow;
+ private List<Bubble> mBubbles;
+
+ public BubbleOverflowAdapter(List<Bubble> list, Consumer<Bubble> promoteBubble) {
+ mBubbles = list;
+ mPromoteBubbleFromOverflow = promoteBubble;
+ }
+
+ @Override
+ public BubbleOverflowAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ BadgedImageView view = (BadgedImageView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.bubble_view, parent, false);
+ view.setPadding(15, 15, 15, 15);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder vh, int index) {
+ Bubble bubble = mBubbles.get(index);
+
+ vh.mBadgedImageView.update(bubble);
+ vh.mBadgedImageView.setOnClickListener(view -> {
+ mBubbles.remove(bubble);
+ notifyDataSetChanged();
+ mPromoteBubbleFromOverflow.accept(bubble);
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return mBubbles.size();
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public BadgedImageView mBadgedImageView;
+
+ public ViewHolder(BadgedImageView v) {
+ super(v);
+ mBadgedImageView = v;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 54a42a6..fe4c915 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -33,6 +33,8 @@
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
@@ -40,6 +42,9 @@
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.InsetDrawable;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
@@ -58,6 +63,7 @@
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
+import android.widget.ImageView;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@@ -195,8 +201,6 @@
private int mPointerHeight;
private int mStatusBarHeight;
private int mImeOffset;
- private int mBubbleMenuOffset = 252;
- private BubbleIconFactory mBubbleIconFactory;
private Bubble mExpandedBubble;
private boolean mIsExpanded;
@@ -320,6 +324,8 @@
private Runnable mAfterMagnet;
private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
+ private BubbleExpandedView mOverflowExpandedView;
+ private ImageView mOverflowBtn;
public BubbleStackView(Context context, BubbleData data,
@Nullable SurfaceSynchronizer synchronizer) {
@@ -369,8 +375,6 @@
mBubbleContainer.setClipChildren(false);
addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
- mBubbleIconFactory = new BubbleIconFactory(context);
-
mExpandedViewContainer = new FrameLayout(context);
mExpandedViewContainer.setElevation(elevation);
mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
@@ -414,6 +418,10 @@
setFocusable(true);
mBubbleContainer.bringToFront();
+ if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
+ setUpOverflow();
+ }
+
setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
if (!mIsExpanded || mIsExpansionAnimating) {
return view.onApplyWindowInsets(insets);
@@ -421,7 +429,13 @@
mExpandedAnimationController.updateYPosition(
// Update the insets after we're done translating otherwise position
// calculation for them won't be correct.
- () -> mExpandedBubble.getExpandedView().updateInsets(insets));
+ () -> {
+ if (mExpandedBubble == null) {
+ mOverflowExpandedView.updateInsets(insets);
+ } else {
+ mExpandedBubble.getExpandedView().updateInsets(insets);
+ }
+ });
return view.onApplyWindowInsets(insets);
});
@@ -433,7 +447,11 @@
// Reposition & adjust the height for new orientation
if (mIsExpanded) {
mExpandedViewContainer.setTranslationY(getExpandedViewY());
- mExpandedBubble.getExpandedView().updateView();
+ if (mExpandedBubble == null) {
+ mOverflowExpandedView.updateView();
+ } else {
+ mExpandedBubble.getExpandedView().updateView();
+ }
}
// Need to update the padding around the view
@@ -499,6 +517,51 @@
mBubbleMenuView = findViewById(R.id.bubble_menu_container);
}
+ private void setUpOverflow() {
+ mOverflowExpandedView = (BubbleExpandedView) mInflater.inflate(
+ R.layout.bubble_expanded_view, this /* root */, false /* attachToRoot */);
+ mOverflowExpandedView.setOverflow(true);
+
+ mInflater.inflate(R.layout.bubble_overflow_button, this);
+ mOverflowBtn = findViewById(R.id.bubble_overflow_button);
+ mOverflowBtn.setOnClickListener(v -> {
+ showOverflow();
+ });
+
+ TypedArray ta = mContext.obtainStyledAttributes(
+ new int[]{android.R.attr.colorBackgroundFloating});
+ int bgColor = ta.getColor(0, Color.WHITE /* default */);
+ ta.recycle();
+
+ InsetDrawable fg = new InsetDrawable(mOverflowBtn.getDrawable(), 28);
+ ColorDrawable bg = new ColorDrawable(bgColor);
+ AdaptiveIconDrawable adaptiveIcon = new AdaptiveIconDrawable(bg, fg);
+ mOverflowBtn.setImageDrawable(adaptiveIcon);
+ mOverflowBtn.setVisibility(GONE);
+ }
+
+ void showOverflow() {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "Show overflow.");
+ }
+ mExpandedViewContainer.setAlpha(0.0f);
+ mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
+ if (mExpandedBubble != null) {
+ mExpandedBubble.setContentVisibility(false);
+ mExpandedBubble = null;
+ }
+ mExpandedViewContainer.removeAllViews();
+ if (mIsExpanded) {
+ mExpandedViewContainer.addView(mOverflowExpandedView);
+ mOverflowExpandedView.populateExpandedView();
+ mExpandedViewContainer.setVisibility(VISIBLE);
+ mExpandedViewContainer.setAlpha(1.0f);
+ mOverflowExpandedView.setContentVisibility(true);
+ }
+ requestUpdate();
+ });
+ }
+
private void setUpFlyout() {
if (mFlyout != null) {
removeView(mFlyout);
@@ -734,9 +797,10 @@
new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
animateInFlyoutForBubble(bubble);
+ updatePointerPosition();
+ updateOverflowBtnVisibility( /*apply */ true);
requestUpdate();
logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
- updatePointerPosition();
}
// via BubbleData.Listener
@@ -753,7 +817,32 @@
} else {
Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
}
- updatePointerPosition();
+ updateOverflowBtnVisibility(/* apply */ true);
+ }
+
+ private void updateOverflowBtnVisibility(boolean apply) {
+ if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
+ return;
+ }
+ if (mIsExpanded) {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "Expanded && overflow > 0. Show overflow button at");
+ Log.d(TAG, "x: " + mExpandedAnimationController.getOverflowBtnLeft());
+ Log.d(TAG, "y: " + mExpandedAnimationController.getExpandedY());
+ }
+ mOverflowBtn.setX(mExpandedAnimationController.getOverflowBtnLeft());
+ mOverflowBtn.setY(mExpandedAnimationController.getExpandedY());
+ mOverflowBtn.setVisibility(VISIBLE);
+ mExpandedAnimationController.setShowOverflowBtn(true);
+ if (apply) {
+ mExpandedAnimationController.expandFromStack(null /* after */);
+ }
+ } else {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "Collapsed. Hide overflow button.");
+ }
+ mOverflowBtn.setVisibility(GONE);
+ }
}
// via BubbleData.Listener
@@ -953,6 +1042,12 @@
final Bubble previouslySelected = mExpandedBubble;
beforeExpandedViewAnimation();
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "animateCollapse");
+ Log.d(TAG, BubbleDebugConfig.formatBubblesString(this.getBubblesOnScreen(),
+ this.getExpandedBubble()));
+ }
+ updateOverflowBtnVisibility(/* apply */ false);
mBubbleContainer.cancelAllAnimations();
mExpandedAnimationController.collapseBackToStack(
mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
@@ -960,7 +1055,9 @@
() -> {
mBubbleContainer.setActiveController(mStackAnimationController);
afterExpandedViewAnimation();
- previouslySelected.setContentVisibility(false);
+ if (previouslySelected != null) {
+ previouslySelected.setContentVisibility(false);
+ }
});
mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
@@ -975,12 +1072,12 @@
beforeExpandedViewAnimation();
mBubbleContainer.setActiveController(mExpandedAnimationController);
+ updateOverflowBtnVisibility(/* apply */ false);
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition();
afterExpandedViewAnimation();
} /* after */);
-
mExpandedViewContainer.setTranslationX(getCollapsedX());
mExpandedViewContainer.setTranslationY(getCollapsedY());
mExpandedViewContainer.setAlpha(0f);
@@ -1063,9 +1160,11 @@
Log.d(TAG, "onDragStart()");
}
if (mIsExpanded || mIsExpansionAnimating) {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "mIsExpanded or mIsExpansionAnimating");
+ }
return;
}
-
hideBubbleMenu();
mStackAnimationController.cancelStackPositionAnimations();
mBubbleContainer.setActiveController(mStackAnimationController);
@@ -1545,7 +1644,11 @@
if (!mExpandedViewYAnim.isRunning()) {
// We're not animating so set the value
mExpandedViewContainer.setTranslationY(y);
- mExpandedBubble.getExpandedView().updateView();
+ if (mExpandedBubble == null) {
+ mOverflowExpandedView.updateView();
+ } else {
+ mExpandedBubble.getExpandedView().updateView();
+ }
} else {
// We are animating so update the value; there is an end listener on the animator
// that will ensure expandedeView.updateView gets called.
@@ -1571,23 +1674,28 @@
}
private void updatePointerPosition() {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "updatePointerPosition()");
- }
-
Bubble expandedBubble = getExpandedBubble();
if (expandedBubble == null) {
return;
}
int index = getBubbleIndex(expandedBubble);
+ if (index >= mMaxBubbles) {
+ // In between state, where extra bubble will be overflowed, and new bubble added
+ index = 0;
+ }
float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
float halfBubble = mBubbleSize / 2f;
float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
// Padding might be adjusted for insets, so get it directly from the view
bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
- expandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
+ if (index >= mMaxBubbles) {
+ Bubble first = mBubbleData.getBubbles().get(0);
+ first.getExpandedView().setPointerPosition(bubbleCenter);
+ } else {
+ expandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
+ }
}
/**
@@ -1680,7 +1788,11 @@
if (!isExpanded()) {
return false;
}
- return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
+ if (mExpandedBubble == null) {
+ return mOverflowExpandedView.performBackPressIfNeeded();
+ } else {
+ return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
+ }
}
/** For debugging only */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 6528f37..6d6969d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -67,6 +67,8 @@
private float mBubblePaddingTop;
/** Size of each bubble. */
private float mBubbleSizePx;
+ /** Width of the overflow button. */
+ private float mOverflowBtnWidth;
/** Height of the status bar. */
private float mStatusBarHeight;
/** Size of display. */
@@ -81,7 +83,7 @@
private boolean mAnimatingExpand = false;
private boolean mAnimatingCollapse = false;
- private Runnable mAfterExpand;
+ private @Nullable Runnable mAfterExpand;
private Runnable mAfterCollapse;
private PointF mCollapsePoint;
@@ -97,6 +99,7 @@
private boolean mSpringingBubbleToTouch = false;
private int mExpandedViewPadding;
+ private boolean mShowOverflowBtn;
public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
int orientation) {
@@ -116,7 +119,7 @@
/**
* Animates expanding the bubbles into a row along the top of the screen.
*/
- public void expandFromStack(Runnable after) {
+ public void expandFromStack(@Nullable Runnable after) {
mAnimatingCollapse = false;
mAnimatingExpand = true;
mAfterExpand = after;
@@ -150,6 +153,14 @@
}
}
+ public void setShowOverflowBtn(boolean showBtn) {
+ mShowOverflowBtn = showBtn;
+ }
+
+ public boolean getShowOverflowBtn() {
+ return mShowOverflowBtn;
+ }
+
/**
* Animates the bubbles along a curved path, either to expand them along the top or collapse
* them back into a stack.
@@ -380,6 +391,7 @@
mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+ mOverflowBtnWidth = mBubbleSizePx;
mStatusBarHeight =
res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
@@ -498,6 +510,14 @@
return getRowLeft() + bubbleFromRowLeft;
}
+ public float getOverflowBtnLeft() {
+ if (mLayout == null || mLayout.getChildCount() == 0) {
+ return 0;
+ }
+ return getBubbleLeft(mLayout.getChildCount() - 1) + mBubbleSizePx
+ + getSpaceBetweenBubbles();
+ }
+
/**
* When expanded, the bubbles are centered in the screen. In portrait, all available space is
* used. In landscape we have too much space so the value is restricted. This method accounts
@@ -505,7 +525,7 @@
*
* @return the desired width to display the expanded bubbles in.
*/
- private float getWidthForDisplayingBubbles() {
+ public float getWidthForDisplayingBubbles() {
final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
// display size y in landscape will be the smaller dimension of the screen
@@ -551,7 +571,11 @@
final float totalBubbleWidth = bubbleCount * mBubbleSizePx;
final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles();
- final float rowWidth = totalGapWidth + totalBubbleWidth;
+ float rowWidth = totalGapWidth + totalBubbleWidth;
+ if (mShowOverflowBtn) {
+ rowWidth += getSpaceBetweenBubbles();
+ rowWidth += mOverflowBtnWidth;
+ }
// This display size we're using includes the size of the insets, we want the true
// center of the display minus the notch here, which means we should include the
@@ -559,7 +583,6 @@
final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
final float halfRow = rowWidth / 2f;
final float rowLeft = trueCenter - halfRow;
-
return rowLeft;
}
@@ -567,12 +590,12 @@
* @return Space between bubbles in row above expanded view.
*/
private float getSpaceBetweenBubbles() {
- final float rowMargins = mExpandedViewPadding * 2;
- final float maxRowWidth = getWidthForDisplayingBubbles() - rowMargins;
-
final float totalBubbleWidth = mBubblesMaxRendered * mBubbleSizePx;
- final float totalGapWidth = maxRowWidth - totalBubbleWidth;
-
+ final float rowMargins = mExpandedViewPadding * 2;
+ float totalGapWidth = getWidthForDisplayingBubbles() - rowMargins - totalBubbleWidth;
+ if (mShowOverflowBtn) {
+ totalGapWidth -= mBubbleSizePx;
+ }
final int gapCount = mBubblesMaxRendered - 1;
final float gapWidth = totalGapWidth / gapCount;
return gapWidth;