Auto bubble some notifications (behind debug flag)
When the flag is flipped notifications that fulfill any
of these will bubble:
- use MessageStyle
- have remote input
- have an app overlay intent
- have priority >= HIGH
Test: atest SystemUITests (no new tests)
Bug: 111236845
Change-Id: Iff86d78a24ab798ddb681a1ad59e1131136814d4
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 70ee747..53862fb 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -22,8 +22,11 @@
import static com.android.systemui.bubbles.BubbleMovementHelper.EDGE_OVERLAP;
+import android.app.Notification;
+import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Point;
+import android.service.notification.StatusBarNotification;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
@@ -33,6 +36,7 @@
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -47,7 +51,13 @@
private static final String TAG = "BubbleController";
+ // Enables some subset of notifs to automatically become bubbles
+ public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
+ // When a bubble is dismissed, recreate it as a notification
+ public static final boolean DEBUG_DEMOTE_TO_NOTIF = false;
+
private Context mContext;
+ private BubbleDismissListener mDismissListener;
private Map<String, BubbleView> mBubbles = new HashMap<>();
private BubbleStackView mStackView;
@@ -56,6 +66,21 @@
// Bubbles get added to the status bar view
private StatusBarWindowController mStatusBarWindowController;
+ /**
+ * Listener to find out about bubble / bubble stack dismissal events.
+ */
+ public interface BubbleDismissListener {
+ /**
+ * Called when the entire stack of bubbles is dismissed by the user.
+ */
+ void onStackDismissed();
+
+ /**
+ * Called when a specific bubble is dismissed by the user.
+ */
+ void onBubbleDismissed(String key);
+ }
+
public BubbleController(Context context) {
mContext = context;
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
@@ -65,6 +90,13 @@
}
/**
+ * Set a listener to be notified of bubble dismissal events.
+ */
+ public void setDismissListener(BubbleDismissListener listener) {
+ mDismissListener = listener;
+ }
+
+ /**
* Whether or not there are bubbles present, regardless of them being visible on the
* screen (e.g. if on AOD).
*/
@@ -99,6 +131,9 @@
for (String key: mBubbles.keySet()) {
removeBubble(key);
}
+ if (mDismissListener != null) {
+ mDismissListener.onStackDismissed();
+ }
}
/**
@@ -146,6 +181,38 @@
mStackView.removeBubble(bv);
bv.getEntry().setBubbleDismissed(true);
}
+ if (mDismissListener != null) {
+ mDismissListener.onBubbleDismissed(key);
+ }
+ }
+
+ /**
+ * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
+ */
+ public void updateVisibility(boolean visible) {
+ if (mStackView == null) {
+ return;
+ }
+ ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
+ for (BubbleView bv : mBubbles.values()) {
+ NotificationData.Entry entry = bv.getEntry();
+ if (entry != null) {
+ if (entry.row.isRemoved() || entry.isBubbleDismissed() || entry.row.isDismissed()) {
+ viewsToRemove.add(bv);
+ }
+ }
+ }
+ for (BubbleView view : viewsToRemove) {
+ mBubbles.remove(view.getKey());
+ mStackView.removeBubble(view);
+ if (mBubbles.size() == 0) {
+ ((ViewGroup) mStatusBarWindowController.getStatusBarView()).removeView(mStackView);
+ mStackView = null;
+ }
+ }
+ if (mStackView != null) {
+ mStackView.setVisibility(visible ? VISIBLE : GONE);
+ }
}
// TODO: factor in PIP location / maybe last place user had it
@@ -166,4 +233,30 @@
return new Point(EDGE_OVERLAP, size);
}
+ /**
+ * Whether the notification should bubble or not.
+ */
+ public static boolean shouldAutoBubble(NotificationData.Entry entry, int priority,
+ boolean canAppOverlay) {
+ if (!DEBUG_ENABLE_AUTO_BUBBLE || entry.isBubbleDismissed()) {
+ return false;
+ }
+ StatusBarNotification n = entry.notification;
+ boolean hasRemoteInput = false;
+ if (n.getNotification().actions != null) {
+ for (Notification.Action action : n.getNotification().actions) {
+ if (action.getRemoteInputs() != null) {
+ hasRemoteInput = true;
+ break;
+ }
+ }
+ }
+ Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
+ boolean shouldBubble = priority >= NotificationManager.IMPORTANCE_HIGH
+ || Notification.MessagingStyle.class.equals(style)
+ || Notification.CATEGORY_MESSAGE.equals(n.getNotification().category)
+ || hasRemoteInput
+ || canAppOverlay;
+ return shouldBubble && !entry.isBubbleDismissed();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index 7581d8c..ea67736 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+
import android.content.Context;
import android.content.res.Resources;
import android.os.Trace;
@@ -25,6 +27,7 @@
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
@@ -62,6 +65,7 @@
Dependency.get(StatusBarStateController.class);
private final NotificationEntryManager mEntryManager =
Dependency.get(NotificationEntryManager.class);
+ private final BubbleController mBubbleController = Dependency.get(BubbleController.class);
// Lazy
private ShadeController mShadeController;
@@ -75,6 +79,41 @@
private NotificationPresenter mPresenter;
private NotificationListContainer mListContainer;
+ private StatusBarStateListener mStatusBarStateListener;
+
+ /**
+ * Listens for the current state of the status bar and updates the visibility state
+ * of bubbles as needed.
+ */
+ public class StatusBarStateListener implements StatusBarStateController.StateListener {
+ private int mState;
+ private BubbleController mController;
+
+ public StatusBarStateListener(BubbleController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Returns the current status bar state.
+ */
+ public int getCurrentState() {
+ return mState;
+ }
+
+ @Override
+ public void onStateChanged(int newState) {
+ mState = newState;
+ // Order here matters because we need to remove the expandable notification row
+ // from it's current parent (NSSL or bubble) before it can be added to the new parent
+ if (mState == SHADE) {
+ updateNotificationViews();
+ mController.updateVisibility(true);
+ } else {
+ mController.updateVisibility(false);
+ updateNotificationViews();
+ }
+ }
+ }
private ShadeController getShadeController() {
if (mShadeController == null) {
@@ -87,6 +126,9 @@
Resources res = context.getResources();
mAlwaysExpandNonGroupedNotification =
res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
+ mStatusBarStateListener = new StatusBarStateListener(mBubbleController);
+ mEntryManager.setStatusBarStateListener(mStatusBarStateListener);
+ Dependency.get(StatusBarStateController.class).addListener(mStatusBarStateListener);
}
public void setUpWithPresenter(NotificationPresenter presenter,
@@ -102,6 +144,7 @@
ArrayList<NotificationData.Entry> activeNotifications = mEntryManager.getNotificationData()
.getActiveNotifications();
ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
+ ArrayList<NotificationData.Entry> toBubble = new ArrayList<>();
final int N = activeNotifications.size();
for (int i = 0; i < N; i++) {
NotificationData.Entry ent = activeNotifications.get(i);
@@ -110,6 +153,14 @@
// temporarily become children if they were isolated before.
continue;
}
+ ent.row.setStatusBarState(mStatusBarStateListener.getCurrentState());
+ boolean showAsBubble = ent.isBubble() && !ent.isBubbleDismissed()
+ && mStatusBarStateListener.getCurrentState() == SHADE;
+ if (showAsBubble) {
+ toBubble.add(ent);
+ continue;
+ }
+
int userId = ent.notification.getUserId();
// Display public version of the notification if we need to redact.
@@ -210,6 +261,12 @@
}
+ for (int i = 0; i < toBubble.size(); i++) {
+ // TODO: might make sense to leave them in the shade and just reposition them
+ NotificationData.Entry ent = toBubble.get(i);
+ mBubbleController.addBubble(ent);
+ }
+
mVisualStabilityManager.onReorderingFinished();
// clear the map again for the next usage
mTmpChildOrderMap.clear();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 274d4b0..ac0fe6e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -15,13 +15,16 @@
*/
package com.android.systemui.statusbar.notification;
+import static com.android.systemui.bubbles.BubbleController.DEBUG_DEMOTE_TO_NOTIF;
import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.notification.row.NotificationInflater.FLAG_CONTENT_VIEW_AMBIENT;
import static com.android.systemui.statusbar.notification.row.NotificationInflater.FLAG_CONTENT_VIEW_HEADS_UP;
import android.annotation.Nullable;
import android.app.Notification;
+import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -62,6 +65,7 @@
import com.android.systemui.InitController;
import com.android.systemui.R;
import com.android.systemui.UiOffloadThread;
+import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.statusbar.AlertingNotificationManager;
import com.android.systemui.statusbar.AmbientPulseManager;
import com.android.systemui.statusbar.NotificationLifetimeExtender;
@@ -72,6 +76,7 @@
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationUiAdjustment;
import com.android.systemui.statusbar.NotificationUpdateHandler;
+import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.notification.NotificationData.KeyguardEnvironment;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -99,7 +104,7 @@
*/
public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,
ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,
- VisualStabilityManager.Callback {
+ VisualStabilityManager.Callback, BubbleController.BubbleDismissListener {
private static final String TAG = "NotificationEntryMgr";
protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
protected static final boolean ENABLE_HEADS_UP = true;
@@ -124,6 +129,7 @@
Dependency.get(ForegroundServiceController.class);
private final AmbientPulseManager mAmbientPulseManager =
Dependency.get(AmbientPulseManager.class);
+ private final BubbleController mBubbleController = Dependency.get(BubbleController.class);
// Lazily retrieved dependencies
private NotificationRemoteInputManager mRemoteInputManager;
@@ -146,6 +152,7 @@
protected final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
= new ArrayList<>();
private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener;
+ private NotificationViewHierarchyManager.StatusBarStateListener mStatusBarStateListener;
private final class NotificationClicker implements View.OnClickListener {
@@ -175,6 +182,11 @@
row.setJustClicked(true);
DejankUtils.postAfterTraversal(() -> row.setJustClicked(false));
+ // If it was a bubble we should close it
+ if (row.getEntry().isBubble()) {
+ mBubbleController.collapseStack();
+ }
+
mCallback.onNotificationClicked(sbn, row);
}
@@ -229,6 +241,7 @@
mDreamManager = IDreamManager.Stub.asInterface(
ServiceManager.checkService(DreamService.DREAM_SERVICE));
mMessagingUtil = new NotificationMessagingUtil(context);
+ mBubbleController.setDismissListener(this /* bubbleEventListener */);
mNotificationData = new NotificationData();
Dependency.get(InitController.class).addPostInitTask(this::onPostInit);
}
@@ -451,6 +464,23 @@
mCallback.onPerformRemoveNotification(n);
}
+ @Override
+ public void onStackDismissed() {
+ updateNotifications();
+ }
+
+ @Override
+ public void onBubbleDismissed(String key) {
+ NotificationData.Entry entry = mNotificationData.get(key);
+ if (entry != null) {
+ entry.setBubbleDismissed(true);
+ if (!DEBUG_DEMOTE_TO_NOTIF) {
+ performRemoveNotification(entry.notification);
+ }
+ }
+ updateNotifications();
+ }
+
/**
* Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
* about the failure.
@@ -727,7 +757,12 @@
if (DEBUG) {
Log.d(TAG, "createNotificationViews(notification=" + sbn + " " + ranking);
}
+
NotificationData.Entry entry = new NotificationData.Entry(sbn, ranking);
+ if (shouldAutoBubble(entry)) {
+ entry.setIsBubble(true);
+ }
+
Dependency.get(LeakDetector.class).trackInstance(entry);
entry.createIcons(mContext, sbn);
// Construct the expanded view.
@@ -948,6 +983,11 @@
}
}
+ public void setStatusBarStateListener(
+ NotificationViewHierarchyManager.StatusBarStateListener listener) {
+ mStatusBarStateListener = listener;
+ }
+
/**
* Whether the notification should peek in from the top and alert the user.
*
@@ -965,6 +1005,14 @@
return false;
}
+ // TODO: need to changes this, e.g. should still heads up in expanded shade, might want
+ // message bubble from the bubble to go through heads up path
+ boolean inShade = mStatusBarStateListener != null
+ && mStatusBarStateListener.getCurrentState() == SHADE;
+ if (entry.isBubble() && !entry.isBubbleDismissed() && inShade) {
+ return false;
+ }
+
if (!canAlertCommon(entry)) {
if (DEBUG) {
Log.d(TAG, "No heads up: notification shouldn't alert: " + sbn.getKey());
@@ -1152,6 +1200,17 @@
}
}
+
+ /**
+ * Whether a bubble is appropriate to auto-bubble or not.
+ */
+ private boolean shouldAutoBubble(NotificationData.Entry entry) {
+ int priority = mNotificationData.getImportance(entry.key);
+ NotificationChannel channel = mNotificationData.getChannel(entry.key);
+ boolean canAppOverlay = channel != null && channel.canOverlayApps();
+ return BubbleController.shouldAutoBubble(entry, priority, canAppOverlay);
+ }
+
/**
* Callback for NotificationEntryManager.
*/