diff options
14 files changed, 483 insertions, 58 deletions
diff --git a/packages/SystemUI/res/drawable/ic_bubble_overflow_button.xml b/packages/SystemUI/res/drawable/ic_bubble_overflow_button.xml new file mode 100644 index 000000000000..64b57c5aac2b --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_bubble_overflow_button.xml @@ -0,0 +1,23 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:viewportHeight="24" + android:viewportWidth="24" + android:height="52dp" + android:width="52dp"> + <path android:fillColor="#1A73E8" + android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/bubble_overflow_activity.xml b/packages/SystemUI/res/layout/bubble_overflow_activity.xml index 4cee74615bd2..95f205a1be34 100644 --- a/packages/SystemUI/res/layout/bubble_overflow_activity.xml +++ b/packages/SystemUI/res/layout/bubble_overflow_activity.xml @@ -13,8 +13,9 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> + <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/bubble_overflow_recycler" - android:scrollbars="vertical" - android:layout_width="wrap_content" - android:layout_height="match_parent"/> + android:layout_gravity="center_horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> diff --git a/packages/SystemUI/res/layout/bubble_overflow_button.xml b/packages/SystemUI/res/layout/bubble_overflow_button.xml new file mode 100644 index 000000000000..eb5dc9b0051a --- /dev/null +++ b/packages/SystemUI/res/layout/bubble_overflow_button.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<ImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_overflow_button" + android:layout_width="@dimen/individual_bubble_size" + android:layout_height="@dimen/individual_bubble_size" + android:src="@drawable/ic_bubble_overflow_button" + android:scaleType="center" + android:layout_gravity="end"/> diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index c1cf7b420078..4171cd974132 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -27,6 +27,12 @@ performance issues arise. --> <integer name="bubbles_max_rendered">5</integer> + <!-- Number of columns in bubble overflow. --> + <integer name="bubbles_overflow_columns">4</integer> + + <!-- Maximum number of bubbles we allow in overflow before we dismiss the oldest one. --> + <integer name="bubbles_max_overflow">16</integer> + <!-- Ratio of "left" end of status bar that will swipe to QQS. --> <integer name="qqs_split_fraction">3</integer> <!-- Ratio of "right" end of status bar that will swipe to QS. --> diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java index dc2499602125..601bae286451 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 @@ public class BadgedImageView extends ImageView { /** * 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 2d9775deb4b5..ccce85ca74fb 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.LauncherApps; 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 @@ class Bubble { } 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 @@ class Bubble { 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 @@ class Bubble { 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 @@ class Bubble { } /** + * 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 e642d4e16802..8c9946fcfc7b 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.lang.annotation.Target; 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mBubbleData = data; mBubbleData.setListener(mBubbleDataListener); + mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered); mNotificationEntryManager = entryManager; mNotificationEntryManager.addNotificationEntryListener(mEntryListener); @@ -370,6 +375,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 cc0824ecc45c..8b687e7114db 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 @@ public class BubbleData { // 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 @@ public class BubbleData { 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 class BubbleData { 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 @@ public class BubbleData { 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 @@ public class BubbleData { mStateChange.orderChanged = true; } mStateChange.addedBubble = bubble; + if (!isExpanded()) { mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId())); // Top bubble becomes selected. @@ -407,6 +428,17 @@ public class BubbleData { 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 @@ public class BubbleData { if (mListener != null && mStateChange.anythingChanged()) { mListener.applyUpdate(mStateChange); } - mStateChange = new Update(mBubbles); + mStateChange = new Update(mBubbles, mOverflowBubbles); } /** @@ -689,12 +721,19 @@ public class BubbleData { } /** - * 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 48ce4e9b0097..cf8b2be1becb 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.DEBUG_BUBBLE_EXPAND 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList + "bubble=" + getBubbleKey()); } try { - if (mBubble.usingShortcutInfo()) { + if (!mIsOverflow && mBubble.usingShortcutInfo()) { mActivityView.startShortcutActivity(mBubble.getShortcutInfo(), options, null /* sourceBounds */); } else { @@ -133,7 +136,7 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList // 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList // 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } 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 4252f72b808e..006de8406ce2 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 @@ public class BubbleExperimentConfig { 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 @@ public class BubbleExperimentConfig { } /** + * 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 d99607fd6236..bea55c820b40 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 @@ import javax.inject.Inject; * Must be public to be accessible to androidx...AppComponentFactory */ public class BubbleOverflowActivity extends Activity { + 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; - private BubbleController mBubbleController; @Inject public BubbleOverflowActivity(BubbleController controller) { @@ -35,17 +66,42 @@ public class BubbleOverflowActivity extends Activity { 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 @@ public class BubbleOverflowActivity extends Activity { 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 54a42a6ec212..fe4c91509057 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.app.Notification; 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.Point; 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; 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { // 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { } 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { () -> { mBubbleContainer.setActiveController(mStackAnimationController); afterExpandedViewAnimation(); - previouslySelected.setContentVisibility(false); + if (previouslySelected != null) { + previouslySelected.setContentVisibility(false); + } }); mExpandedViewXAnim.animateToFinalPosition(getCollapsedX()); @@ -975,12 +1072,12 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { } 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 @@ public class BubbleStackView extends FrameLayout { 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 6528f3762473..6d6969da8c8a 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 @@ public class ExpandedAnimationController 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 @@ public class ExpandedAnimationController 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 @@ public class ExpandedAnimationController private boolean mSpringingBubbleToTouch = false; private int mExpandedViewPadding; + private boolean mShowOverflowBtn; public ExpandedAnimationController(Point displaySize, int expandedViewPadding, int orientation) { @@ -116,7 +119,7 @@ public class ExpandedAnimationController /** * 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 class ExpandedAnimationController } } + 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 @@ public class ExpandedAnimationController 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 @@ public class ExpandedAnimationController 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 @@ public class ExpandedAnimationController * * @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 @@ public class ExpandedAnimationController 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 @@ public class ExpandedAnimationController final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f; final float halfRow = rowWidth / 2f; final float rowLeft = trueCenter - halfRow; - return rowLeft; } @@ -567,12 +590,12 @@ public class ExpandedAnimationController * @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; diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java index b09603d78ecb..1a2e23796c78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java @@ -257,7 +257,7 @@ public class BubbleDataTest extends SysuiTestCase { * enforced by expiring the bubble which was least recently updated (lowest timestamp). */ @Test - public void test_collapsed_addBubble_atMaxBubbles_expiresOldest() { + public void test_collapsed_addBubble_atMaxBubbles_overflowsOldest() { // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryA2, 2000); @@ -269,7 +269,10 @@ public class BubbleDataTest extends SysuiTestCase { // Test sendUpdatedEntryAtTime(mEntryC1, 6000); verifyUpdateReceived(); + + // Verify assertBubbleRemoved(mBubbleA1, BubbleController.DISMISS_AGED); + assertThat(mBubbleData.getOverflowBubbles()).isEqualTo(ImmutableList.of(mBubbleA1)); } /** |