Merge "Bubble overflow - core functionality"

This commit is contained in:
TreeHugger Robot
2020-01-24 09:59:50 +00:00
committed by Android (Google) Code Review
14 changed files with 484 additions and 59 deletions

View File

@@ -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>

View File

@@ -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"/>

View File

@@ -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"/>

View File

@@ -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. -->

View File

@@ -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();
}

View File

@@ -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);
}
/**
@@ -261,6 +282,13 @@ class Bubble {
setShowDot(false /* show */, true /* animate */);
}
/**
* 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.

View File

@@ -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));
}
}
};

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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}
@@ -140,6 +143,16 @@ public class BubbleExperimentConfig {
ALLOW_BUBBLE_MENU_DEFAULT ? 1 : 0) != 0;
}
/**
* 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

View File

@@ -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 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 @@ 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;
}
}
}

View File

@@ -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 */

View File

@@ -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;

View File

@@ -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));
}
/**