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!