Add flag to allowing the system to make BubbleMetadata for a notification
- Add experiment class to try and keep experimented related code in
- If notification has info to bubble & is allowed, make BubbleMetadata for
it (but don't actually bubble)
- Options for only allowing messages to bubble
- Alter NotificationEntry to have settable BubbleMetadata
- Add notion of user created bubbles & user promoting / demoting content
to a bubble; user created bubbles can't be canceled or finished, the
user must explictly dismiss the notif
- Move all of our Dependency.get into constructor & use inject
Test: it compiles (see manual tests in CL with the menu affordance)
Test: atest BubbleControllerTest
Bug: 138116133
Bug: 143173197
Change-Id: I6100bce7b74146afd4e0c4c02d4ce7731fecd5af
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index f8e45d4..c6b9090 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -64,7 +64,8 @@
private long mLastUpdated;
private long mLastAccessed;
- private boolean mIsRemoved;
+
+ private boolean mIsUserCreated;
/**
* Whether this notification should be shown in the shade when it is also displayed as a bubble.
@@ -74,9 +75,7 @@
*/
private boolean mShowInShadeWhenBubble = true;
- /**
- * Whether the bubble should show a dot for the notification indicating updated content.
- */
+ /** Whether the bubble should show a dot for the notification indicating updated content. */
private boolean mShowBubbleUpdateDot = true;
/** Whether flyout text should be suppressed, regardless of any other flags or state. */
@@ -294,6 +293,20 @@
return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
}
+ /**
+ * Whether this bubble was explicitly created by the user via a SysUI affordance.
+ */
+ boolean isUserCreated() {
+ return mIsUserCreated;
+ }
+
+ /**
+ * Set whether this bubble was explicitly created by the user via a SysUI affordance.
+ */
+ void setUserCreated(boolean isUserCreated) {
+ mIsUserCreated = isUserCreated;
+ }
+
float getDesiredHeight(Context context) {
Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
boolean useRes = data.getDesiredHeightResId() != 0;
@@ -319,9 +332,8 @@
@Nullable
PendingIntent getBubbleIntent(Context context) {
- Notification notif = mEntry.getSbn().getNotification();
- Notification.BubbleMetadata data = notif.getBubbleMetadata();
- if (BubbleController.canLaunchInActivityView(context, mEntry) && data != null) {
+ Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
+ if (data != null) {
return data.getIntent();
}
return null;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 0231b56..9f7bdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -67,7 +67,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -82,6 +81,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ZenModeController;
@@ -96,6 +96,8 @@
import javax.inject.Inject;
import javax.inject.Singleton;
+import dagger.Lazy;
+
/**
* Bubbles are a special type of content that can "float" on top of other apps or System UI.
* Bubbles can be expanded to show more content.
@@ -132,6 +134,7 @@
private BubbleExpandListener mExpandListener;
@Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
private final NotificationGroupManager mNotificationGroupManager;
+ private final Lazy<ShadeController> mShadeController;
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
@@ -206,24 +209,34 @@
}
@Inject
- public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
- BubbleData data, ConfigurationController configurationController,
- NotificationInterruptionStateProvider interruptionStateProvider,
- ZenModeController zenModeController,
- NotificationLockscreenUserManager notifUserManager,
- NotificationGroupManager groupManager) {
- this(context, statusBarWindowController, data, null /* synchronizer */,
- configurationController, interruptionStateProvider, zenModeController,
- notifUserManager, groupManager);
- }
-
- public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
- BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
+ public BubbleController(Context context,
+ StatusBarWindowController statusBarWindowController,
+ StatusBarStateController statusBarStateController,
+ Lazy<ShadeController> shadeController,
+ BubbleData data,
ConfigurationController configurationController,
NotificationInterruptionStateProvider interruptionStateProvider,
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager,
- NotificationGroupManager groupManager) {
+ NotificationGroupManager groupManager,
+ NotificationEntryManager entryManager) {
+ this(context, statusBarWindowController, statusBarStateController, shadeController,
+ data, null /* synchronizer */, configurationController, interruptionStateProvider,
+ zenModeController, notifUserManager, groupManager, entryManager);
+ }
+
+ public BubbleController(Context context,
+ StatusBarWindowController statusBarWindowController,
+ StatusBarStateController statusBarStateController,
+ Lazy<ShadeController> shadeController,
+ BubbleData data,
+ @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
+ ConfigurationController configurationController,
+ NotificationInterruptionStateProvider interruptionStateProvider,
+ ZenModeController zenModeController,
+ NotificationLockscreenUserManager notifUserManager,
+ NotificationGroupManager groupManager,
+ NotificationEntryManager entryManager) {
mContext = context;
mNotificationInterruptionStateProvider = interruptionStateProvider;
mNotifUserManager = notifUserManager;
@@ -249,7 +262,7 @@
mBubbleData = data;
mBubbleData.setListener(mBubbleDataListener);
- mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
+ mNotificationEntryManager = entryManager;
mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
mNotificationGroupManager = groupManager;
@@ -271,9 +284,10 @@
}
});
+ mShadeController = shadeController;
mStatusBarWindowController = statusBarWindowController;
mStatusBarStateListener = new StatusBarStateListener();
- Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
+ statusBarStateController.addCallback(mStatusBarStateListener);
mTaskStackListener = new BubbleTaskStackListener();
ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
@@ -497,15 +511,45 @@
* @param notif the notification associated with this bubble.
*/
void updateBubble(NotificationEntry notif) {
- updateBubble(notif, /* supressFlyout */ false);
+ updateBubble(notif, false /* suppressFlyout */);
}
void updateBubble(NotificationEntry notif, boolean suppressFlyout) {
+ updateBubble(notif, suppressFlyout, true /* showInShade */);
+ }
+
+ void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
// If this is an interruptive notif, mark that it's interrupted
if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
notif.setInterruption();
}
- mBubbleData.notificationEntryUpdated(notif, suppressFlyout);
+ mBubbleData.notificationEntryUpdated(notif, suppressFlyout, showInShade);
+ }
+
+ /**
+ * Called when a user has indicated that an active notification should be shown as a bubble.
+ * <p>
+ * This method will collapse the shade, create the bubble without a flyout or dot, and suppress
+ * the notification from appearing in the shade.
+ *
+ * @param entry the notification to show as a bubble.
+ */
+ public void onUserCreatedBubbleFromNotification(NotificationEntry entry) {
+ mShadeController.get().collapsePanel(true);
+ entry.setFlagBubble(true);
+ updateBubble(entry, true /* suppressFlyout */, false /* showInShade */);
+ mBubbleData.getBubbleWithKey(entry.getKey()).setUserCreated(true);
+ }
+
+ /**
+ * Called when a user has indicated that an active notification appearing as a bubble should
+ * no longer be shown as a bubble.
+ *
+ * @param entry the notification to no longer show as a bubble.
+ */
+ public void onUserDemotedBubbleFromNotification(NotificationEntry entry) {
+ entry.setFlagBubble(false);
+ removeBubble(entry.getKey(), DISMISS_BLOCKED);
}
/**
@@ -571,7 +615,7 @@
mNotificationEntryManager.updateNotifications(
"BubbleController.onNotificationRemoveRequested");
return true;
- } else if (!userRemovedNotif && entry != null) {
+ } else if (!userRemovedNotif && entry != null && !bubble.isUserCreated()) {
// This wasn't a user removal so we should remove the bubble as well
mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
return false;
@@ -631,6 +675,9 @@
private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
@Override
public void onPendingEntryAdded(NotificationEntry entry) {
+ Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
+ BubbleExperimentConfig.adjustForExperiments(mContext, entry, b);
+
if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
&& canLaunchInActivityView(mContext, entry)) {
updateBubble(entry);
@@ -639,13 +686,15 @@
@Override
public void onPreEntryUpdated(NotificationEntry entry) {
+ Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
+ BubbleExperimentConfig.adjustForExperiments(mContext, entry, b);
+
boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
&& canLaunchInActivityView(mContext, entry);
if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.getKey())) {
// It was previously a bubble but no longer a bubble -- lets remove it
removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
} else if (shouldBubble) {
- Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
updateBubble(entry);
}
}
@@ -949,19 +998,26 @@
PendingIntent intent = entry.getBubbleMetadata() != null
? entry.getBubbleMetadata().getIntent()
: null;
+ return canLaunchIntentInActivityView(context, entry, intent);
+ }
+
+ static boolean canLaunchIntentInActivityView(Context context, NotificationEntry entry,
+ PendingIntent intent) {
if (intent == null) {
- Log.w(TAG, "Unable to create bubble -- no intent");
+ Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
return false;
}
ActivityInfo info =
intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0);
if (info == null) {
- Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
+ Log.w(TAG, "Unable to send as bubble, "
+ + entry.getKey() + " couldn't find activity info for intent: "
+ intent);
return false;
}
if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
- Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
+ Log.w(TAG, "Unable to send as bubble, "
+ + entry.getKey() + " activity is not resizable for intent: "
+ intent);
return false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 4e229c0..f4d48b2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -179,7 +179,8 @@
dispatchPendingChanges();
}
- void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout) {
+ void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout,
+ boolean showInShade) {
if (DEBUG_BUBBLE_DATA) {
Log.d(TAG, "notificationEntryUpdated: " + entry);
}
@@ -208,7 +209,7 @@
setSelectedBubbleInternal(bubble);
}
boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
- bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected);
+ bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected && showInShade);
bubble.setShowBubbleDot(!isBubbleExpandedAndSelected);
dispatchPendingChanges();
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 1d9f6b2..e9c19d2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -184,10 +184,12 @@
+ " mActivityViewStatus=" + mActivityViewStatus
+ " bubble=" + getBubbleKey());
}
- if (mBubble != null) {
- // Must post because this is called from a binder thread.
- post(() -> mBubbleController.removeBubble(mBubble.getKey(),
- BubbleController.DISMISS_TASK_FINISHED));
+ if (mBubble != null && !mBubble.isUserCreated()) {
+ if (mBubble != null) {
+ // Must post because this is called from a binder thread.
+ post(() -> mBubbleController.removeBubble(mBubble.getKey(),
+ BubbleController.DISMISS_TASK_FINISHED));
+ }
}
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
new file mode 100644
index 0000000..b478a72
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2019 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 android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.provider.Settings;
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+/**
+ * Common class for experiments controlled via secure settings.
+ */
+public class BubbleExperimentConfig {
+
+ private static final String ALLOW_ANY_NOTIF_TO_BUBBLE = "allow_any_notif_to_bubble";
+ private static final boolean ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT = false;
+
+ private static final String ALLOW_MESSAGE_NOTIFS_TO_BUBBLE = "allow_message_notifs_to_bubble";
+ private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_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}
+ * object will be created by the system and added to the notification.
+ *
+ * This does not produce a bubble, only adds the metadata. It should be used in conjunction
+ * with {@see #allowNotifBubbleMenu} which shows an affordance to bubble notification content.
+ */
+ static boolean allowAnyNotifToBubble(Context context) {
+ return Settings.Secure.getInt(context.getContentResolver(),
+ ALLOW_ANY_NOTIF_TO_BUBBLE,
+ ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT ? 1 : 0) != 0;
+ }
+
+ /**
+ * Same as {@link #allowAnyNotifToBubble(Context)} except it filters for notifications that
+ * are using {@link Notification.MessagingStyle} and have remote input.
+ */
+ static boolean allowMessageNotifsToBubble(Context context) {
+ return Settings.Secure.getInt(context.getContentResolver(),
+ ALLOW_MESSAGE_NOTIFS_TO_BUBBLE,
+ ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_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.
+ */
+ static void adjustForExperiments(Context context, NotificationEntry entry,
+ Bubble previousBubble) {
+ if (entry.getBubbleMetadata() != null) {
+ // Has metadata, nothing to do.
+ return;
+ }
+
+ Notification notification = entry.getSbn().getNotification();
+ boolean isMessage = Notification.MessagingStyle.class.equals(
+ notification.getNotificationStyle());
+ boolean bubbleNotifForExperiment = (isMessage && allowMessageNotifsToBubble(context))
+ || allowAnyNotifToBubble(context);
+
+ final PendingIntent intent = notification.contentIntent;
+ if (bubbleNotifForExperiment
+ && BubbleController.canLaunchIntentInActivityView(context, entry, intent)) {
+ final Icon smallIcon = entry.getSbn().getNotification().getSmallIcon();
+ Notification.BubbleMetadata.Builder metadata =
+ new Notification.BubbleMetadata.Builder()
+ .setDesiredHeight(10000)
+ .setIcon(smallIcon)
+ .setIntent(intent);
+ entry.setBubbleMetadata(metadata.build());
+ }
+
+ if (previousBubble != null) {
+ // Update to a previously user-created bubble, set its flag now so the update goes
+ // to the bubble.
+ entry.setFlagBubble(true);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java
index 7d09932..66ed864 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java
@@ -18,7 +18,6 @@
import static com.android.systemui.statusbar.StatusBarState.SHADE;
-import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.database.ContentObserver;
@@ -189,8 +188,7 @@
return false;
}
- final Notification n = sbn.getNotification();
- if (n.getBubbleMetadata() == null || n.getBubbleMetadata().getIntent() == null) {
+ if (entry.getBubbleMetadata() == null || entry.getBubbleMetadata().getIntent() == null) {
if (DEBUG) {
Log.d(TAG, "No bubble up: notification: " + sbn.getKey()
+ " doesn't have valid metadata");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index a4c8fc4..28ccaf5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -113,6 +113,7 @@
private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
public CharSequence remoteInputText;
private final List<Person> mAssociatedPeople = new ArrayList<>();
+ private Notification.BubbleMetadata mBubbleMetadata;
/**
* If {@link android.app.RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
@@ -199,6 +200,7 @@
}
mSbn = sbn;
+ mBubbleMetadata = mSbn.getNotification().getBubbleMetadata();
updatePeopleList();
}
@@ -340,7 +342,33 @@
* Returns the data needed for a bubble for this notification, if it exists.
*/
public Notification.BubbleMetadata getBubbleMetadata() {
- return mSbn.getNotification().getBubbleMetadata();
+ return mBubbleMetadata;
+ }
+
+ /**
+ * Sets bubble metadata for this notification.
+ */
+ public void setBubbleMetadata(Notification.BubbleMetadata metadata) {
+ mBubbleMetadata = metadata;
+ }
+
+ /**
+ * Updates the {@link Notification#FLAG_BUBBLE} flag on this notification to indicate
+ * whether it is a bubble or not. If this entry is set to not bubble, or does not have
+ * the required info to bubble, the flag cannot be set to true.
+ *
+ * @param shouldBubble whether this notification should be flagged as a bubble.
+ * @return true if the value changed.
+ */
+ public boolean setFlagBubble(boolean shouldBubble) {
+ boolean wasBubble = isBubble();
+ if (!shouldBubble) {
+ mSbn.getNotification().flags &= ~FLAG_BUBBLE;
+ } else if (mBubbleMetadata != null && canBubble()) {
+ // wants to be bubble & can bubble, set flag
+ mSbn.getNotification().flags |= FLAG_BUBBLE;
+ }
+ return wasBubble != isBubble();
}
/**