Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2ac2d3aa authored by Mady Mellor's avatar Mady Mellor
Browse files

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
parent 871fe0a9
Loading
Loading
Loading
Loading
+9 −4
Original line number Diff line number Diff line
@@ -5608,11 +5608,14 @@ package android.app {
  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 @@ package android.app {
  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);
  }
+146 −44
Original line number Diff line number Diff line
@@ -8542,13 +8542,16 @@ public class Notification implements Parcelable
     * 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 @@ public class Notification implements Parcelable
        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,33 +8600,60 @@ public class Notification implements Parcelable
        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) {
            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.
         */
@@ -8631,17 +8663,28 @@ public class Notification implements Parcelable
        }

        /**
         * @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 @@ public class Notification implements Parcelable

        /**
         * @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 @@ public class Notification implements Parcelable

        @Override
        public void writeToParcel(Parcel out, int flags) {
            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 @@ public class Notification implements Parcelable
                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 @@ public class Notification implements Parcelable
            @DimenRes private int mDesiredHeightResId;
            private int mFlags;
            private PendingIntent mDeleteIntent;
            private String mShortcutId;

            /**
             * Constructs a new builder object.
@@ -8741,50 +8795,98 @@ public class Notification implements Parcelable
            }

            /**
             * 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>The shortcut icon will be used to represent the bubble when it is collapsed.</p>
             *
             * <p>An intent is required.</p>
             * <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>
             *
             * @throws IllegalArgumentException if intent is null
             * <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>
             * <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 shape of a bubble icon is adaptive and will match the device theme.
             * <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>
             *
             * Ideally your icon should be constructed via
             * {@link Icon#createWithAdaptiveBitmap(Bitmap)}, otherwise, the icon will be shrunk
             * and placed on an adaptive shape.
             * <p>Calling this method will clear the contents of
             * {@link #createShortcutBubble(String)} if it was previously called on this builder.
             * </p>
             *
             * @throws IllegalArgumentException if intent is null.
             * @throws IllegalArgumentException if icon is null.
             */
            @NonNull
            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;
            }

            /**
             * @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 app content defined by
             * {@link #setIntent(PendingIntent)}.
             * 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 @@ public class Notification implements Parcelable


            /**
             * 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 @@ public class Notification implements Parcelable

            /**
             * 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 @@ public class Notification implements Parcelable
            /**
             * 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;
            }
+2 −2
Original line number Diff line number Diff line
@@ -346,14 +346,14 @@ class Bubble {
     * 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;
    }
+5 −1
Original line number Diff line number Diff line
@@ -1075,8 +1075,12 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
     */
    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;
+3 −2
Original line number Diff line number Diff line
@@ -357,7 +357,7 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList

            if (isNew) {
                mBubbleIntent = mBubble.getBubbleIntent();
                if (mBubbleIntent != null) {
                if (mBubbleIntent != null || mBubble.getShortcutInfo() != null) {
                    setContentVisibility(false);
                    mActivityView.setVisibility(VISIBLE);
                }
@@ -543,7 +543,8 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
    }

    private boolean usingActivityView() {
        return mBubbleIntent != null && mActivityView != null;
        return (mBubbleIntent != null || mBubble.getShortcutInfo() != null)
                && mActivityView != null;
    }

    /**
Loading