Bubble API: Allow developers to create BubbleMetadata from a shortcut id
Adds an option to specify a bubble via a shortcutId. If the builder had
icon / intent previously specified, the shortcutId will clobber those
and vice versa.
Unfortunately, this means that BubbleMetadata#getIcon and #getIntent may
return null when previously they were non-null. I've deprecated these
getters and introduced new ones along with new setters so that the naming
remains consistent.
NoMan will check if that shortcut exists before applying FLAG_BUBBLE.
Update SystemUI to use BubbleMetadata builder with shortcut id for the
shortcut experiment.
Adds test to ensure the launcher apps callback is added / removed
appropriately & that notifs with shortcut bubbles are properly flagged
or unflagged.
Test: atest NotificationManagerServiceTest
Test: atest NotificationTest NotificationManagerTest (see CTS CL)
Bug: 138116133
Bug: 144352570
Change-Id: I2e8155edc7fd70d6978fc80310f071bf6510e0d2
diff --git a/api/current.txt b/api/current.txt
index 1c03c23..1ade863 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5608,11 +5608,14 @@
public static final class Notification.BubbleMetadata implements android.os.Parcelable {
method public int describeContents();
method public boolean getAutoExpandBubble();
+ method @Nullable public android.graphics.drawable.Icon getBubbleIcon();
+ method @Nullable public android.app.PendingIntent getBubbleIntent();
method @Nullable public android.app.PendingIntent getDeleteIntent();
method @Dimension(unit=android.annotation.Dimension.DP) public int getDesiredHeight();
method @DimenRes public int getDesiredHeightResId();
- method @NonNull public android.graphics.drawable.Icon getIcon();
- method @NonNull public android.app.PendingIntent getIntent();
+ method @Deprecated @NonNull public android.graphics.drawable.Icon getIcon();
+ method @Deprecated @NonNull public android.app.PendingIntent getIntent();
+ method @Nullable public String getShortcutId();
method public boolean isNotificationSuppressed();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.Notification.BubbleMetadata> CREATOR;
@@ -5621,12 +5624,14 @@
public static final class Notification.BubbleMetadata.Builder {
ctor public Notification.BubbleMetadata.Builder();
method @NonNull public android.app.Notification.BubbleMetadata build();
+ method @NonNull public android.app.Notification.BubbleMetadata.Builder createIntentBubble(@NonNull android.app.PendingIntent, @NonNull android.graphics.drawable.Icon);
+ method @NonNull public android.app.Notification.BubbleMetadata.Builder createShortcutBubble(@NonNull String);
method @NonNull public android.app.Notification.BubbleMetadata.Builder setAutoExpandBubble(boolean);
method @NonNull public android.app.Notification.BubbleMetadata.Builder setDeleteIntent(@Nullable android.app.PendingIntent);
method @NonNull public android.app.Notification.BubbleMetadata.Builder setDesiredHeight(@Dimension(unit=android.annotation.Dimension.DP) int);
method @NonNull public android.app.Notification.BubbleMetadata.Builder setDesiredHeightResId(@DimenRes int);
- method @NonNull public android.app.Notification.BubbleMetadata.Builder setIcon(@NonNull android.graphics.drawable.Icon);
- method @NonNull public android.app.Notification.BubbleMetadata.Builder setIntent(@NonNull android.app.PendingIntent);
+ method @Deprecated @NonNull public android.app.Notification.BubbleMetadata.Builder setIcon(@NonNull android.graphics.drawable.Icon);
+ method @Deprecated @NonNull public android.app.Notification.BubbleMetadata.Builder setIntent(@NonNull android.app.PendingIntent);
method @NonNull public android.app.Notification.BubbleMetadata.Builder setSuppressNotification(boolean);
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 6f63eea..3c4e861 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -8542,13 +8542,16 @@
* Encapsulates the information needed to display a notification as a bubble.
*
* <p>A bubble is used to display app content in a floating window over the existing
- * foreground activity. A bubble has a collapsed state represented by an icon,
- * {@link BubbleMetadata.Builder#setIcon(Icon)} and an expanded state which is populated
- * via {@link BubbleMetadata.Builder#setIntent(PendingIntent)}.</p>
+ * foreground activity. A bubble has a collapsed state represented by an icon and an
+ * expanded state that displays an activity. These may be defined via
+ * {@link BubbleMetadata.Builder#createIntentBubble(PendingIntent, Icon)} or they may
+ * be definied via an existing shortcut using
+ * {@link BubbleMetadata.Builder#createShortcutBubble(String)}.
+ * </p>
*
* <b>Notifications with a valid and allowed bubble will display in collapsed state
* outside of the notification shade on unlocked devices. When a user interacts with the
- * collapsed bubble, the bubble intent will be invoked and displayed.</b>
+ * collapsed bubble, the bubble activity will be invoked and displayed.</b>
*
* @see Notification.Builder#setBubbleMetadata(BubbleMetadata)
*/
@@ -8560,10 +8563,12 @@
private int mDesiredHeight;
@DimenRes private int mDesiredHeightResId;
private int mFlags;
+ private String mShortcutId;
/**
* If set and the app creating the bubble is in the foreground, the bubble will be posted
- * in its expanded state, with the contents of {@link #getIntent()} in a floating window.
+ * in its expanded state, with the contents of {@link #getBubbleIntent()} in a floating
+ * window.
*
* <p>This flag has no effect if the app posting the bubble is not in the foreground.
* The app is considered foreground if it is visible and on the screen, note that
@@ -8595,34 +8600,61 @@
public static final int FLAG_SUPPRESS_NOTIFICATION = 0x00000002;
private BubbleMetadata(PendingIntent expandIntent, PendingIntent deleteIntent,
- Icon icon, int height, @DimenRes int heightResId) {
+ Icon icon, int height, @DimenRes int heightResId, String shortcutId) {
mPendingIntent = expandIntent;
mIcon = icon;
mDesiredHeight = height;
mDesiredHeightResId = heightResId;
mDeleteIntent = deleteIntent;
+ mShortcutId = shortcutId;
}
private BubbleMetadata(Parcel in) {
- mPendingIntent = PendingIntent.CREATOR.createFromParcel(in);
- mIcon = Icon.CREATOR.createFromParcel(in);
+ if (in.readInt() != 0) {
+ mPendingIntent = PendingIntent.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ }
mDesiredHeight = in.readInt();
mFlags = in.readInt();
if (in.readInt() != 0) {
mDeleteIntent = PendingIntent.CREATOR.createFromParcel(in);
}
mDesiredHeightResId = in.readInt();
+ if (in.readInt() != 0) {
+ mShortcutId = in.readString();
+ }
}
/**
- * @return the pending intent used to populate the floating window for this bubble.
+ * @return the shortcut id used to populate the bubble, if it exists.
*/
+ @Nullable
+ public String getShortcutId() {
+ return mShortcutId;
+ }
+
+ /**
+ * @deprecated use {@link #getBubbleIntent()} or use {@link #getShortcutId()} if created
+ * with a valid shortcut instead.
+ */
+ @Deprecated
@NonNull
public PendingIntent getIntent() {
return mPendingIntent;
}
/**
+ * @return the pending intent used to populate the floating window for this bubble, or
+ * null if this bubble is shortcut based.
+ */
+ @Nullable
+ public PendingIntent getBubbleIntent() {
+ return mPendingIntent;
+ }
+
+ /**
* @return the pending intent to send when the bubble is dismissed by a user, if one exists.
*/
@Nullable
@@ -8631,17 +8663,28 @@
}
/**
- * @return the icon that will be displayed for this bubble when it is collapsed.
+ * @deprecated use {@link #getBubbleIcon()} or use {@link #getShortcutId()} if created
+ * with a valid shortcut instead.
*/
+ @Deprecated
@NonNull
public Icon getIcon() {
return mIcon;
}
/**
+ * @return the icon that will be displayed for this bubble when it is collapsed, or null
+ * if the bubble is shortcut based.
+ */
+ @Nullable
+ public Icon getBubbleIcon() {
+ return mIcon;
+ }
+
+ /**
* @return the ideal height, in DPs, for the floating window that app content defined by
- * {@link #getIntent()} for this bubble. A value of 0 indicates a desired height has not
- * been set.
+ * {@link #getBubbleIntent()} for this bubble. A value of 0 indicates a desired height has
+ * not been set.
*/
@Dimension(unit = DP)
public int getDesiredHeight() {
@@ -8650,7 +8693,7 @@
/**
* @return the resId of ideal height for the floating window that app content defined by
- * {@link #getIntent()} for this bubble. A value of 0 indicates a res value has not
+ * {@link #getBubbleIntent()} for this bubble. A value of 0 indicates a res value has not
* been provided for the desired height.
*/
@DimenRes
@@ -8697,8 +8740,14 @@
@Override
public void writeToParcel(Parcel out, int flags) {
- mPendingIntent.writeToParcel(out, 0);
- mIcon.writeToParcel(out, 0);
+ out.writeInt(mPendingIntent != null ? 1 : 0);
+ if (mPendingIntent != null) {
+ mPendingIntent.writeToParcel(out, 0);
+ }
+ out.writeInt(mIcon != null ? 1 : 0);
+ if (mIcon != null) {
+ mIcon.writeToParcel(out, 0);
+ }
out.writeInt(mDesiredHeight);
out.writeInt(mFlags);
out.writeInt(mDeleteIntent != null ? 1 : 0);
@@ -8706,6 +8755,10 @@
mDeleteIntent.writeToParcel(out, 0);
}
out.writeInt(mDesiredHeightResId);
+ out.writeInt(TextUtils.isEmpty(mShortcutId) ? 0 : 1);
+ if (!TextUtils.isEmpty(mShortcutId)) {
+ out.writeString(mShortcutId);
+ }
}
/**
@@ -8733,6 +8786,7 @@
@DimenRes private int mDesiredHeightResId;
private int mFlags;
private PendingIntent mDeleteIntent;
+ private String mShortcutId;
/**
* Constructs a new builder object.
@@ -8741,50 +8795,98 @@
}
/**
- * Sets the intent that will be used when the bubble is expanded. This will display the
- * app content in a floating window over the existing foreground activity.
+ * Creates a {@link BubbleMetadata.Builder} based on a shortcut. Only
+ * {@link android.content.pm.ShortcutManager#addDynamicShortcuts(List)} shortcuts are
+ * supported.
*
- * <p>An intent is required.</p>
+ * <p>The shortcut icon will be used to represent the bubble when it is collapsed.</p>
*
- * @throws IllegalArgumentException if intent is null
+ * <p>The shortcut activity will be used when the bubble is expanded. This will display
+ * the shortcut activity in a floating window over the existing foreground activity.</p>
+ *
+ * <p>If the shortcut has not been published when the bubble notification is sent,
+ * no bubble will be produced. If the shortcut is deleted while the bubble is active,
+ * the bubble will be removed.</p>
+ *
+ * <p>Calling this method will clear the contents of
+ * {@link #createIntentBubble(PendingIntent, Icon)} if it was previously called on
+ * this builder.</p>
*/
@NonNull
- public BubbleMetadata.Builder setIntent(@NonNull PendingIntent intent) {
- if (intent == null) {
- throw new IllegalArgumentException("Bubble requires non-null pending intent");
+ public BubbleMetadata.Builder createShortcutBubble(@NonNull String shortcutId) {
+ if (!TextUtils.isEmpty(shortcutId)) {
+ // If shortcut id is set, we don't use these if they were previously set.
+ mPendingIntent = null;
+ mIcon = null;
}
- mPendingIntent = intent;
+ mShortcutId = shortcutId;
return this;
}
/**
- * Sets the icon that will represent the bubble when it is collapsed.
+ * Creates a {@link BubbleMetadata.Builder} based on the provided intent and icon.
*
- * <p>An icon is required and should be representative of the content within the bubble.
- * If your app produces multiple bubbles, the image should be unique for each of them.
+ * <p>The icon will be used to represent the bubble when it is collapsed. An icon
+ * should be representative of the content within the bubble. If your app produces
+ * multiple bubbles, the icon should be unique for each of them.</p>
+ *
+ * <p>The intent that will be used when the bubble is expanded. This will display the
+ * app content in a floating window over the existing foreground activity.</p>
+ *
+ * <p>Calling this method will clear the contents of
+ * {@link #createShortcutBubble(String)} if it was previously called on this builder.
* </p>
*
- * <p>The shape of a bubble icon is adaptive and will match the device theme.
- *
- * Ideally your icon should be constructed via
- * {@link Icon#createWithAdaptiveBitmap(Bitmap)}, otherwise, the icon will be shrunk
- * and placed on an adaptive shape.
- * </p>
- *
+ * @throws IllegalArgumentException if intent is null.
* @throws IllegalArgumentException if icon is null.
*/
@NonNull
- public BubbleMetadata.Builder setIcon(@NonNull Icon icon) {
+ public BubbleMetadata.Builder createIntentBubble(@NonNull PendingIntent intent,
+ @NonNull Icon icon) {
+ if (intent == null) {
+ throw new IllegalArgumentException("Bubble requires non-null pending intent");
+ }
if (icon == null) {
throw new IllegalArgumentException("Bubbles require non-null icon");
}
+ mShortcutId = null;
+ mPendingIntent = intent;
mIcon = icon;
return this;
}
/**
- * Sets the desired height in DPs for the app content defined by
- * {@link #setIntent(PendingIntent)}.
+ * @deprecated use {@link #createIntentBubble(PendingIntent, Icon)}
+ * or {@link #createShortcutBubble(String)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public BubbleMetadata.Builder setIntent(@NonNull PendingIntent intent) {
+ if (intent == null) {
+ throw new IllegalArgumentException("Bubble requires non-null pending intent");
+ }
+ mShortcutId = null;
+ mPendingIntent = intent;
+ return this;
+ }
+
+ /**
+ * @deprecated use {@link #createIntentBubble(PendingIntent, Icon)}
+ * or {@link #createShortcutBubble(String)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public BubbleMetadata.Builder setIcon(@NonNull Icon icon) {
+ if (icon == null) {
+ throw new IllegalArgumentException("Bubbles require non-null icon");
+ }
+ mShortcutId = null;
+ mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Sets the desired height in DPs for the expanded content of the bubble.
*
* <p>This height may not be respected if there is not enough space on the screen or if
* the provided height is too small to be useful.</p>
@@ -8806,8 +8908,7 @@
/**
- * Sets the desired height via resId for the app content defined by
- * {@link #setIntent(PendingIntent)}.
+ * Sets the desired height via resId for the expanded content of the bubble.
*
* <p>This height may not be respected if there is not enough space on the screen or if
* the provided height is too small to be useful.</p>
@@ -8829,7 +8930,7 @@
/**
* Sets whether the bubble will be posted in its expanded state (with the contents of
- * {@link #getIntent()} in a floating window).
+ * {@link #getBubbleIntent()} in a floating window).
*
* <p>This flag has no effect if the app posting the bubble is not in the foreground.
* The app is considered foreground if it is visible and on the screen, note that
@@ -8882,20 +8983,21 @@
/**
* Creates the {@link BubbleMetadata} defined by this builder.
*
- * @throws IllegalStateException if {@link #setIntent(PendingIntent)} and/or
- * {@link #setIcon(Icon)} have not been called on this
- * builder.
+ * @throws IllegalStateException if neither {@link #createShortcutBubble(String)} or
+ * {@link #createIntentBubble(PendingIntent, Icon)} have been called on this builder.
*/
@NonNull
public BubbleMetadata build() {
- if (mPendingIntent == null) {
- throw new IllegalStateException("Must supply pending intent to bubble");
+ if (mShortcutId == null && mPendingIntent == null) {
+ throw new IllegalStateException(
+ "Must supply pending intent or shortcut to bubble");
}
- if (mIcon == null) {
- throw new IllegalStateException("Must supply an icon for the bubble");
+ if (mShortcutId == null && mIcon == null) {
+ throw new IllegalStateException(
+ "Must supply an icon or shortcut for the bubble");
}
BubbleMetadata data = new BubbleMetadata(mPendingIntent, mDeleteIntent,
- mIcon, mDesiredHeight, mDesiredHeightResId);
+ mIcon, mDesiredHeight, mDesiredHeightResId, mShortcutId);
data.setFlags(mFlags);
return data;
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 77c8e0b..b89305e 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -346,14 +346,14 @@
* To populate the icon use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}.
*/
boolean usingShortcutInfo() {
- return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent());
+ return mEntry.getBubbleMetadata().getShortcutId() != null;
}
@Nullable
PendingIntent getBubbleIntent() {
Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
if (data != null) {
- return data.getIntent();
+ return data.getBubbleIntent();
}
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 644d8c4..e642d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -1075,8 +1075,12 @@
*/
static boolean canLaunchInActivityView(Context context, NotificationEntry entry) {
PendingIntent intent = entry.getBubbleMetadata() != null
- ? entry.getBubbleMetadata().getIntent()
+ ? entry.getBubbleMetadata().getBubbleIntent()
: null;
+ if (entry.getBubbleMetadata() != null
+ && entry.getBubbleMetadata().getShortcutId() != null) {
+ return true;
+ }
if (intent == null) {
Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
return false;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index c1705db..5c46496 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -357,7 +357,7 @@
if (isNew) {
mBubbleIntent = mBubble.getBubbleIntent();
- if (mBubbleIntent != null) {
+ if (mBubbleIntent != null || mBubble.getShortcutInfo() != null) {
setContentVisibility(false);
mActivityView.setVisibility(VISIBLE);
}
@@ -543,7 +543,8 @@
}
private boolean usingActivityView() {
- return mBubbleIntent != null && mActivityView != null;
+ return (mBubbleIntent != 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 4c1cf49..4252f72 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -29,7 +29,6 @@
import android.app.PendingIntent;
import android.app.Person;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Color;
@@ -182,7 +181,7 @@
ShortcutInfo info = getShortcutInfo(context, entry.getSbn().getPackageName(),
entry.getSbn().getUser(), shortcutId);
if (info != null) {
- metadata = createForShortcut(context, entry);
+ metadata = createForShortcut(shortcutId);
}
// Replace existing metadata with shortcut, or we're bubbling for experiment
@@ -259,30 +258,17 @@
}
if (intent != null) {
return new Notification.BubbleMetadata.Builder()
+ .createIntentBubble(intent, icon)
.setDesiredHeight(BUBBLE_HEIGHT)
- .setIcon(icon)
- .setIntent(intent)
.build();
}
return null;
}
- static Notification.BubbleMetadata createForShortcut(Context context, NotificationEntry entry) {
- // ShortcutInfo does not return an icon, instead a Drawable, lets just use
- // notification icon for BubbleMetadata.
- Icon icon = entry.getSbn().getNotification().getSmallIcon();
-
- // ShortcutInfo does not return the intent, lets make a fake but identifiable
- // intent so we can still add bubbleMetadata
- if (sDummyShortcutIntent == null) {
- Intent i = new Intent(SHORTCUT_DUMMY_INTENT);
- sDummyShortcutIntent = PendingIntent.getActivity(context, 0, i,
- PendingIntent.FLAG_UPDATE_CURRENT);
- }
+ static Notification.BubbleMetadata createForShortcut(String shortcutId) {
return new Notification.BubbleMetadata.Builder()
.setDesiredHeight(BUBBLE_HEIGHT)
- .setIcon(icon)
- .setIntent(sDummyShortcutIntent)
+ .createShortcutBubble(shortcutId)
.build();
}
@@ -304,10 +290,6 @@
: null;
}
- static boolean isShortcutIntent(PendingIntent intent) {
- return intent != null && intent.equals(sDummyShortcutIntent);
- }
-
static List<Person> getPeopleFromNotification(NotificationEntry entry) {
Bundle extras = entry.getSbn().getNotification().extras;
ArrayList<Person> personList = new ArrayList<>();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
index b32dbb7..3b818db 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
@@ -18,6 +18,7 @@
import android.app.Notification;
import android.content.Context;
import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
@@ -48,16 +49,19 @@
/**
* Returns the drawable that the developer has provided to display in the bubble.
*/
- Drawable getBubbleDrawable(Bubble b, Context context) {
- if (b.getShortcutInfo() != null && b.usingShortcutInfo()) {
+ Drawable getBubbleDrawable(Context context, ShortcutInfo shortcutInfo,
+ Notification.BubbleMetadata metadata) {
+ if (shortcutInfo != null) {
LauncherApps launcherApps =
(LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
int density = context.getResources().getConfiguration().densityDpi;
- return launcherApps.getShortcutIconDrawable(b.getShortcutInfo(), density);
+ return launcherApps.getShortcutIconDrawable(shortcutInfo, density);
} else {
- Notification.BubbleMetadata metadata = b.getEntry().getBubbleMetadata();
- Icon ic = metadata.getIcon();
- return ic.loadDrawable(context);
+ Icon ic = metadata.getBubbleIcon();
+ if (ic != null) {
+ return ic.loadDrawable(context);
+ }
+ return null;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
index 41f5028..8924cd6 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
@@ -126,15 +126,22 @@
StatusBarNotification sbn = b.getEntry().getSbn();
String packageName = sbn.getPackageName();
- // Shortcut info for this bubble
- String shortcutId = sbn.getNotification().getShortcutId();
- if (BubbleExperimentConfig.useShortcutInfoToBubble(c)
- && shortcutId != null) {
- info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c,
- packageName,
- sbn.getUser(), shortcutId);
+ // Real shortcut info for this bubble
+ String bubbleShortcutId = b.getEntry().getBubbleMetadata().getShortcutId();
+ if (bubbleShortcutId != null) {
+ info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c, packageName,
+ sbn.getUser(), bubbleShortcutId);
+ } else {
+ // Check for experimental shortcut
+ String shortcutId = sbn.getNotification().getShortcutId();
+ if (BubbleExperimentConfig.useShortcutInfoToBubble(c) && shortcutId != null) {
+ info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c,
+ packageName,
+ sbn.getUser(), shortcutId);
+ }
}
+
// App name & app icon
PackageManager pm = c.getPackageManager();
ApplicationInfo appInfo;
@@ -158,7 +165,8 @@
}
// Badged bubble image
- Drawable bubbleDrawable = iconFactory.getBubbleDrawable(b, c);
+ Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo,
+ b.getEntry().getBubbleMetadata());
BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon);
info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable,
badgeBitmapInfo).icon;
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 66ed864..bbf2dde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java
@@ -188,7 +188,9 @@
return false;
}
- if (entry.getBubbleMetadata() == null || entry.getBubbleMetadata().getIntent() == null) {
+ if (entry.getBubbleMetadata() == null
+ || (entry.getBubbleMetadata().getShortcutId() == null
+ && entry.getBubbleMetadata().getBubbleIntent() == null)) {
if (DEBUG) {
Log.d(TAG, "No bubble up: notification: " + sbn.getKey()
+ " doesn't have valid metadata");
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 c4ae409..7daf922 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -952,9 +952,8 @@
long postTime) {
// BubbleMetadata
Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder()
- .setIntent(mExpandIntent)
+ .createIntentBubble(mExpandIntent, Icon.createWithResource("", 0))
.setDeleteIntent(mDeleteIntent)
- .setIcon(Icon.createWithResource("", 0))
.build();
// Notification -> BubbleMetadata
Notification notification = mNotificationTestHelper.createNotification(false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInterruptionStateProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInterruptionStateProviderTest.java
index 677a6fc..1693e7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInterruptionStateProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInterruptionStateProviderTest.java
@@ -557,8 +557,8 @@
private NotificationEntry createBubble() {
Notification.BubbleMetadata data = new Notification.BubbleMetadata.Builder()
- .setIntent(PendingIntent.getActivity(mContext, 0, new Intent(), 0))
- .setIcon(Icon.createWithResource(mContext.getResources(), R.drawable.android))
+ .createIntentBubble(PendingIntent.getActivity(mContext, 0, new Intent(), 0),
+ Icon.createWithResource(mContext.getResources(), R.drawable.android))
.build();
Notification n = new Notification.Builder(getContext(), "a")
.setContentTitle("title")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
index 61e43b0..457bbe2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
@@ -383,9 +383,9 @@
PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, 0);
return new BubbleMetadata.Builder()
- .setIntent(bubbleIntent)
+ .createIntentBubble(bubbleIntent,
+ Icon.createWithResource(mContext, R.drawable.android))
.setDeleteIntent(deleteIntent)
- .setIcon(Icon.createWithResource(mContext, R.drawable.android))
.setDesiredHeight(314)
.build();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
index 9ae477e..7be7da3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
@@ -195,8 +195,8 @@
new Intent(mContext, BubblesTestActivity.class), 0);
mBubbleSbn = new SbnBuilder(mSbn).setBubbleMetadata(
new Notification.BubbleMetadata.Builder()
- .setIntent(bubbleIntent)
- .setIcon(Icon.createWithResource(mContext, R.drawable.android)).build())
+ .createIntentBubble(bubbleIntent,
+ Icon.createWithResource(mContext, R.drawable.android)).build())
.build();
mBubbleEntry = new NotificationEntryBuilder().setSbn(mBubbleSbn).build();
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 385d84a..acb0c70 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -51,6 +51,8 @@
import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_FOREGROUND_SERVICE;
import static android.content.Context.BIND_NOT_PERCEPTIBLE;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED;
import static android.content.pm.PackageManager.FEATURE_LEANBACK;
import static android.content.pm.PackageManager.FEATURE_TELEVISION;
import static android.content.pm.PackageManager.MATCH_ALL;
@@ -147,10 +149,12 @@
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
+import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
+import android.content.pm.ShortcutInfo;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -279,6 +283,7 @@
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
@@ -399,6 +404,7 @@
private RoleObserver mRoleObserver;
private UserManager mUm;
private IPlatformCompat mPlatformCompat;
+ private LauncherApps mLauncherAppsService;
final IBinder mForegroundToken = new Binder();
private WorkerHandler mHandler;
@@ -1195,12 +1201,7 @@
final StatusBarNotification n = r.sbn;
final int callingUid = n.getUid();
final String pkg = n.getPackageName();
- if (isBubble && isNotificationAppropriateToBubble(r, pkg, callingUid,
- null /* oldEntry */)) {
- r.getNotification().flags |= FLAG_BUBBLE;
- } else {
- r.getNotification().flags &= ~FLAG_BUBBLE;
- }
+ applyFlagBubble(r, pkg, callingUid, null /* oldEntry */, isBubble);
}
}
}
@@ -1575,6 +1576,80 @@
}
};
+ // Key: packageName Value: <shortcutId, notifId>
+ private HashMap<String, HashMap<String, String>> mActiveShortcutBubbles = new HashMap<>();
+
+ private boolean mLauncherAppsCallbackRegistered;
+
+ // Bubbles can be created based on a shortcut, we need to listen for changes to
+ // that shortcut so that we may update the bubble appropriately.
+ private final LauncherApps.Callback mLauncherAppsCallback = new LauncherApps.Callback() {
+ @Override
+ public void onPackageRemoved(String packageName, UserHandle user) {
+ }
+
+ @Override
+ public void onPackageAdded(String packageName, UserHandle user) {
+ }
+
+ @Override
+ public void onPackageChanged(String packageName, UserHandle user) {
+ }
+
+ @Override
+ public void onPackagesAvailable(String[] packageNames, UserHandle user,
+ boolean replacing) {
+ }
+
+ @Override
+ public void onPackagesUnavailable(String[] packageNames, UserHandle user,
+ boolean replacing) {
+ }
+
+ @Override
+ public void onShortcutsChanged(@NonNull String packageName,
+ @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
+ HashMap<String, String> shortcutBubbles = mActiveShortcutBubbles.get(packageName);
+ boolean isAppForeground = packageName != null
+ && mActivityManager.getPackageImportance(packageName) == IMPORTANCE_FOREGROUND;
+ ArrayList<String> bubbleKeysToRemove = new ArrayList<>();
+ if (shortcutBubbles != null) {
+ // If we can't find one of our bubbles in the shortcut list, that bubble needs
+ // to be removed.
+ for (String shortcutId : shortcutBubbles.keySet()) {
+ boolean foundShortcut = false;
+ for (int i = 0; i < shortcuts.size(); i++) {
+ if (shortcuts.get(i).getId().equals(shortcutId)) {
+ foundShortcut = true;
+ break;
+ }
+ }
+ if (!foundShortcut) {
+ bubbleKeysToRemove.add(shortcutBubbles.get(shortcutId));
+ }
+ }
+ }
+
+ // Do the removals
+ for (int i = 0; i < bubbleKeysToRemove.size(); i++) {
+ // update flag bubble
+ String bubbleKey = bubbleKeysToRemove.get(i);
+ synchronized (mNotificationLock) {
+ NotificationRecord r = mNotificationsByKey.get(bubbleKey);
+ if (r != null) {
+ final StatusBarNotification n = r.sbn;
+ final int callingUid = n.getUid();
+ final String pkg = n.getPackageName();
+ applyFlagBubble(r, pkg, callingUid, null /* oldEntry */, isAppForeground);
+ mHandler.post(new EnqueueNotificationRunnable(user.getIdentifier(), r,
+ false /* isAppForeground */));
+ }
+ }
+ }
+ }
+ };
+
+
private final class SettingsObserver extends ContentObserver {
private final Uri NOTIFICATION_BADGING_URI
= Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BADGING);
@@ -1658,6 +1733,11 @@
}
@VisibleForTesting
+ void setLauncherApps(LauncherApps launcherApps) {
+ mLauncherAppsService = launcherApps;
+ }
+
+ @VisibleForTesting
void setHints(int hints) {
mListenerHints = hints;
}
@@ -2151,6 +2231,8 @@
mRoleObserver = new RoleObserver(getContext().getSystemService(RoleManager.class),
mPackageManager, getContext().getMainExecutor());
mRoleObserver.init();
+ mLauncherAppsService =
+ (LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE);
} else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
// This observer will force an update when observe is called, causing us to
// bind to listener services.
@@ -5439,19 +5521,18 @@
}
/**
- * Updates the flags for this notification to reflect whether it is a bubble or not.
+ * Updates the flags for this notification to reflect whether it is a bubble or not. Some
+ * bubble specific flags only work if the app is foreground, this will strip those flags
+ * if the app wasn't foreground.
*/
- private void flagNotificationForBubbles(NotificationRecord r, String pkg, int userId,
+ private void updateNotificationBubbleFlags(NotificationRecord r, String pkg, int userId,
NotificationRecord oldRecord, boolean isAppForeground) {
Notification notification = r.getNotification();
- if (isNotificationAppropriateToBubble(r, pkg, userId, oldRecord)) {
- notification.flags |= FLAG_BUBBLE;
- } else {
- notification.flags &= ~FLAG_BUBBLE;
- }
+ applyFlagBubble(r, pkg, userId, oldRecord, true /* desiredFlag */);
+
+ // Remove any bubble specific flags that only work when foregrounded
Notification.BubbleMetadata metadata = notification.getBubbleMetadata();
if (!isAppForeground && metadata != null) {
- // Remove any flags that only work when foregrounded
int flags = metadata.getFlags();
flags &= ~Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
flags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
@@ -5460,6 +5541,54 @@
}
/**
+ * Handles actually applying or removing {@link Notification#FLAG_BUBBLE}. Performs necessary
+ * checks for the provided record to see if it can actually be a bubble.
+ * Tracks shortcut based bubbles so that we can find out if they've changed or been removed.
+ */
+ private void applyFlagBubble(NotificationRecord r, String pkg, int userId,
+ NotificationRecord oldRecord, boolean desiredFlag) {
+ boolean applyFlag = desiredFlag
+ && isNotificationAppropriateToBubble(r, pkg, userId, oldRecord);
+ final String shortcutId = r.getNotification().getBubbleMetadata() != null
+ ? r.getNotification().getBubbleMetadata().getShortcutId()
+ : null;
+ if (applyFlag) {
+ if (shortcutId != null) {
+ // Must track shortcut based bubbles in case the shortcut is removed
+ HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get(
+ r.sbn.getPackageName());
+ if (packageBubbles == null) {
+ packageBubbles = new HashMap<>();
+ }
+ packageBubbles.put(shortcutId, r.getKey());
+ mActiveShortcutBubbles.put(r.sbn.getPackageName(), packageBubbles);
+ if (!mLauncherAppsCallbackRegistered) {
+ mLauncherAppsService.registerCallback(mLauncherAppsCallback, mHandler);
+ mLauncherAppsCallbackRegistered = true;
+ }
+ }
+ r.getNotification().flags |= FLAG_BUBBLE;
+ } else {
+ if (shortcutId != null) {
+ // No longer track shortcut
+ HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get(
+ r.sbn.getPackageName());
+ if (packageBubbles != null) {
+ packageBubbles.remove(shortcutId);
+ }
+ if (packageBubbles != null && packageBubbles.isEmpty()) {
+ mActiveShortcutBubbles.remove(r.sbn.getPackageName());
+ }
+ if (mLauncherAppsCallbackRegistered && mActiveShortcutBubbles.isEmpty()) {
+ mLauncherAppsService.unregisterCallback(mLauncherAppsCallback);
+ mLauncherAppsCallbackRegistered = false;
+ }
+ }
+ r.getNotification().flags &= ~FLAG_BUBBLE;
+ }
+ }
+
+ /**
* @return whether the provided notification record is allowed to be represented as a bubble,
* accounting for user choice & policy.
*/
@@ -5533,10 +5662,6 @@
// no log: no need to inform dev if they didn't attach bubble metadata
return false;
}
- if (!canLaunchInActivityView(getContext(), metadata.getIntent(), pkg)) {
- // no log: method has the failure log
- return false;
- }
if (!mPreferencesHelper.bubblesEnabled()) {
logBubbleError(r.getKey(), "bubbles disabled for user: " + userId);
return false;
@@ -5551,7 +5676,20 @@
"bubbles for channel " + r.getChannel().getId() + " disabled");
return false;
}
- return true;
+
+ String shortcutId = metadata.getShortcutId();
+ boolean shortcutValid = shortcutId != null
+ && hasValidShortcutInfo(shortcutId, pkg, r.getUser());
+ if (metadata.getBubbleIntent() == null && !shortcutValid) {
+ // Should have a shortcut if intent is null
+ logBubbleError(r.getKey(), "couldn't find shortcutId for bubble: " + shortcutId);
+ return false;
+ }
+ if (shortcutValid) {
+ return true;
+ }
+ // no log: canLaunch method has the failure log
+ return canLaunchInActivityView(getContext(), metadata.getBubbleIntent(), pkg);
}
private boolean hasValidRemoteInput(Notification n) {
@@ -5570,6 +5708,22 @@
return false;
}
+ private boolean hasValidShortcutInfo(String shortcutId, String packageName, UserHandle user) {
+ LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery();
+ if (packageName != null) {
+ query.setPackage(packageName);
+ }
+ if (shortcutId != null) {
+ query.setShortcutIds(Arrays.asList(shortcutId));
+ }
+ query.setQueryFlags(FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED);
+ List<ShortcutInfo> shortcuts = mLauncherAppsService.getShortcuts(query, user);
+ ShortcutInfo shortcutInfo = shortcuts != null && shortcuts.size() > 0
+ ? shortcuts.get(0)
+ : null;
+ return shortcutInfo != null;
+ }
+
private void logBubbleError(String key, String failureMessage) {
if (DBG) {
Log.w(TAG, "Bubble notification: " + key + " failed: " + failureMessage);
@@ -6025,7 +6179,7 @@
final String tag = n.getTag();
// We need to fix the notification up a little for bubbles
- flagNotificationForBubbles(r, pkg, callingUid, old, isAppForeground);
+ updateNotificationBubbleFlags(r, pkg, callingUid, old, isAppForeground);
// Handle grouped notifications and bail out early if we
// can to avoid extracting signals.
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index c6c64c9..8799669 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -100,8 +100,10 @@
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
+import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
+import android.content.pm.ShortcutInfo;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.graphics.Color;
@@ -214,6 +216,8 @@
@Mock
private AudioManager mAudioManager;
@Mock
+ private LauncherApps mLauncherApps;
+ @Mock
ActivityManager mActivityManager;
@Mock
Resources mResources;
@@ -420,6 +424,7 @@
mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
mService.setAudioManager(mAudioManager);
+ mService.setLauncherApps(mLauncherApps);
// Tests call directly into the Binder.
mBinderService = mService.getBinderService();
@@ -618,8 +623,8 @@
private Notification.BubbleMetadata.Builder getBubbleMetadataBuilder() {
PendingIntent pi = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
return new Notification.BubbleMetadata.Builder()
- .setIntent(pi)
- .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon));
+ .createIntentBubble(pi,
+ Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon));
}
private Notification.Builder getMessageStyleNotifBuilder(boolean addBubbleMetadata,
@@ -5689,6 +5694,58 @@
}
@Test
+ public void testNotificationBubbles_flagRemoved_whenShortcutRemoved()
+ throws RemoteException {
+ // Bubbles are allowed!
+ setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
+
+ ArgumentCaptor<LauncherApps.Callback> launcherAppsCallback =
+ ArgumentCaptor.forClass(LauncherApps.Callback.class);
+
+ // Messaging notification with shortcut info
+ Notification.BubbleMetadata metadata =
+ getBubbleMetadataBuilder().createShortcutBubble("someshortcutId").build();
+ Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+ null /* groupKey */, false /* isSummary */);
+ nb.setBubbleMetadata(metadata);
+ StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+ "tag", mUid, 0, nb.build(), new UserHandle(mUid), null, 0);
+ NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+
+ // Pretend the shortcut exists
+ List<ShortcutInfo> shortcutInfos = new ArrayList<>();
+ shortcutInfos.add(mock(ShortcutInfo.class));
+ when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos);
+
+ // Test: Send the bubble notification
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.sbn.getTag(),
+ nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId());
+ waitForIdle();
+
+ // Verify:
+
+ // Make sure we register the callback for shortcut changes
+ verify(mLauncherApps, times(1)).registerCallback(launcherAppsCallback.capture(), any());
+
+ // yes allowed, yes messaging w/shortcut, yes bubble
+ Notification notif = mService.getNotificationRecord(nr.sbn.getKey()).getNotification();
+ assertTrue(notif.isBubbleNotification());
+
+ // Test: Remove the shortcut
+ launcherAppsCallback.getValue().onShortcutsChanged(PKG, Collections.emptyList(),
+ new UserHandle(mUid));
+
+ // Verify:
+
+ // Make sure callback is unregistered
+ verify(mLauncherApps, times(1)).unregisterCallback(launcherAppsCallback.getValue());
+
+ // We're no longer a bubble
+ Notification notif2 = mService.getNotificationRecord(nr.sbn.getKey()).getNotification();
+ assertFalse(notif2.isBubbleNotification());
+ }
+
+ @Test
public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryDismissed()
throws Exception {
// Bubbles are allowed!