diff --git a/app/src/main/java/androidx/core/app/NotificationCompat.java b/app/src/main/java/androidx/core/app/NotificationCompat.java new file mode 100644 index 00000000..58bad9cb --- /dev/null +++ b/app/src/main/java/androidx/core/app/NotificationCompat.java @@ -0,0 +1,9614 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.core.app; + +import static androidx.annotation.Dimension.DP; +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.util.Objects.requireNonNull; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.LocusId; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.SystemClock; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.text.style.TextAppearanceSpan; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.RemoteViews; + +import androidx.annotation.ColorInt; +import androidx.annotation.DimenRes; +import androidx.annotation.Dimension; +import androidx.annotation.DoNotInline; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.core.R; +import androidx.core.content.ContextCompat; +import androidx.core.content.LocusIdCompat; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.core.text.BidiFormatter; +import androidx.core.view.GravityCompat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing features in {@link android.app.Notification}. + */ +public class NotificationCompat { + private static final String TAG = "NotifCompat"; + + /** + * An activity that provides a user interface for adjusting notification preferences for its + * containing application. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String INTENT_CATEGORY_NOTIFICATION_PREFERENCES = + "android.intent.category.NOTIFICATION_PREFERENCES"; + + /** + * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will + * contain a {@link NotificationChannelCompat#getId() channel id} that can be used to narrow + * down what settings should be shown in the target app. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID"; + + /** + * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will + * contain a {@link NotificationChannelGroupCompat#getId() group id} that can be used to narrow + * down what settings should be shown in the target app. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID"; + + /** + * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will + * contain the tag provided to + * {@link NotificationManagerCompat#notify(String, int, Notification)} + * that can be used to narrow down what settings should be shown in the target app. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_NOTIFICATION_TAG = "android.intent.extra.NOTIFICATION_TAG"; + + /** + * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will + * contain the id provided to + * {@link NotificationManagerCompat#notify(String, int, Notification)} + * that can be used to narrow down what settings should be shown in the target app. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_NOTIFICATION_ID = "android.intent.extra.NOTIFICATION_ID"; + + /** + * Use all default values (where applicable). + */ + public static final int DEFAULT_ALL = ~0; + + /** + * Use the default notification sound. This will ignore any sound set using + * {@link Builder#setSound} + * + *
+ * A notification that is noisy is more likely to be presented as a heads-up notification, + * on some platforms. + *
+ * + * @see Builder#setDefaults + */ + public static final int DEFAULT_SOUND = 1; + + /** + * Use the default notification vibrate. This will ignore any vibrate set using + * {@link Builder#setVibrate}. Using phone vibration requires the + * {@link android.Manifest.permission#VIBRATE VIBRATE} permission. + * + *+ * A notification that vibrates is more likely to be presented as a heads-up notification, + * on some platforms. + *
+ * + * @see Builder#setDefaults + */ + public static final int DEFAULT_VIBRATE = 2; + + /** + * Use the default notification lights. This will ignore the + * {@link #FLAG_SHOW_LIGHTS} bit, and values set with {@link Builder#setLights}. + * + * @see Builder#setDefaults + */ + public static final int DEFAULT_LIGHTS = 4; + + /** + * Use this constant as the value for audioStreamType to request that + * the default stream type for notifications be used. Currently the + * default stream type is {@link AudioManager#STREAM_NOTIFICATION}. + */ + public static final int STREAM_DEFAULT = -1; + /** + * Bit set in the Notification flags field when LEDs should be turned on + * for this notification. + */ + public static final int FLAG_SHOW_LIGHTS = 0x00000001; + + /** + * Bit set in the Notification flags field if this notification is in + * reference to something that is ongoing, like a phone call. It should + * not be set if this notification is in reference to something that + * happened at a particular point in time, like a missed phone call. + */ + public static final int FLAG_ONGOING_EVENT = 0x00000002; + + /** + * Bit set in the Notification flags field if + * the audio will be repeated until the notification is + * cancelled or the notification window is opened. + */ + public static final int FLAG_INSISTENT = 0x00000004; + + /** + * Bit set in the Notification flags field if the notification's sound, + * vibrate and ticker should only be played if the notification is not already showing. + */ + public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008; + + /** + * Bit set in the Notification flags field if the notification should be canceled when + * it is clicked by the user. + */ + public static final int FLAG_AUTO_CANCEL = 0x00000010; + + /** + * Bit set in the Notification flags field if the notification should not be canceled + * when the user clicks the Clear all button. + */ + public static final int FLAG_NO_CLEAR = 0x00000020; + + /** + * Bit set in the Notification flags field if this notification represents a currently + * running service. This will normally be set for you by + * {@link android.app.Service#startForeground}. + */ + public static final int FLAG_FOREGROUND_SERVICE = 0x00000040; + + /** + * Obsolete flag indicating high-priority notifications; use the priority field instead. + * + * @deprecated Use {@link NotificationCompat.Builder#setPriority(int)} with a positive value. + */ + @Deprecated + public static final int FLAG_HIGH_PRIORITY = 0x00000080; + + /** + * Bit set in the Notification flags field if this notification is relevant to the current + * device only and it is not recommended that it bridge to other devices. + */ + public static final int FLAG_LOCAL_ONLY = 0x00000100; + + /** + * Bit set in the Notification flags field if this notification is the group summary for a + * group of notifications. Grouped notifications may display in a cluster or stack on devices + * which support such rendering. Requires a group key also be set using + * {@link Builder#setGroup}. + */ + public static final int FLAG_GROUP_SUMMARY = 0x00000200; + + /** + * Bit set in the Notification flags field if this notification is showing as a bubble. + * + * Applications cannot set this flag directly; they should instead call + * {@link NotificationCompat.Builder#setBubbleMetadata(BubbleMetadata)} to request that a + * notification be displayed as a bubble, and then check this flag to see whether that request + * was honored by the system. + */ + public static final int FLAG_BUBBLE = 0x00001000; + + /** + * Default notification priority for {@link NotificationCompat.Builder#setPriority(int)}. + * If your application does not prioritize its own notifications, + * use this value for all notifications. + */ + public static final int PRIORITY_DEFAULT = 0; + + /** + * Lower notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for items that are less important. The UI may choose to show + * these items smaller, or at a different position in the list, + * compared with your app's {@link #PRIORITY_DEFAULT} items. + */ + public static final int PRIORITY_LOW = -1; + + /** + * Lowest notification priority for {@link NotificationCompat.Builder#setPriority(int)}; + * these items might not be shown to the user except under + * special circumstances, such as detailed notification logs. + */ + public static final int PRIORITY_MIN = -2; + + /** + * Higher notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for more important notifications or alerts. The UI may choose + * to show these items larger, or at a different position in + * notification lists, compared with your app's {@link #PRIORITY_DEFAULT} items. + */ + public static final int PRIORITY_HIGH = 1; + + /** + * Highest notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for your application's most important items that require the user's + * prompt attention or input. + */ + public static final int PRIORITY_MAX = 2; + + /** + * {@link #getExtras extras} key: this is the title of the notification, + * as supplied to {@link Builder#setContentTitle(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_TITLE = "android.title"; + + /** + * {@link #getExtras extras} key: this is the title of the notification when shown in expanded + * form, e.g. as supplied to {@link BigTextStyle#setBigContentTitle(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_TITLE_BIG = EXTRA_TITLE + ".big"; + + /** + * {@link #getExtras extras} key: this is the main text payload, as supplied to + * {@link Builder#setContentText(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_TEXT = "android.text"; + + /** + * {@link #getExtras extras} key: this is a third line of text, as supplied to + * {@link Builder#setSubText(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_SUB_TEXT = "android.subText"; + + /** + * {@link #getExtras extras} key: this is the remote input history, as supplied to + * {@link Builder#setRemoteInputHistory(CharSequence[])}. + * + * Apps can fill this through {@link Builder#setRemoteInputHistory(CharSequence[])} + * with the most recent inputs that have been sent through a {@link RemoteInput} of this + * Notification and are expected to clear it once the it is no longer relevant (e.g. for chat + * notifications once the other party has responded). + * + * The extra with this key is of type CharSequence[] and contains the most recent entry at + * the 0 index, the second most recent at the 1 index, etc. + * + * @see Builder#setRemoteInputHistory(CharSequence[]) + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_REMOTE_INPUT_HISTORY = "android.remoteInputHistory"; + + /** + * {@link #getExtras extras} key: this is a small piece of additional text as supplied to + * {@link Builder#setContentInfo(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_INFO_TEXT = "android.infoText"; + + /** + * {@link #getExtras extras} key: this is a line of summary information intended to be shown + * alongside expanded notifications, as supplied to (e.g.) + * {@link BigTextStyle#setSummaryText(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_SUMMARY_TEXT = "android.summaryText"; + + /** + * {@link #getExtras extras} key: this is the longer text shown in the big form of a + * {@link BigTextStyle} notification, as supplied to + * {@link BigTextStyle#bigText(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_BIG_TEXT = "android.bigText"; + + /** + * {@link #getExtras extras} key: this is the resource ID of the notification's main small icon, + * as supplied to {@link Builder#setSmallIcon(int)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_SMALL_ICON = "android.icon"; + + /** + * {@link #getExtras extras} key: this is a bitmap to be used instead of the small icon when + * showing the notification payload, as + * supplied to {@link Builder#setLargeIcon(android.graphics.Bitmap)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_LARGE_ICON = "android.largeIcon"; + + /** + * {@link #getExtras extras} key: this is a bitmap to be used instead of the one from + * {@link Builder#setLargeIcon(android.graphics.Bitmap)} when the notification is + * shown in its expanded form, as supplied to + * {@link BigPictureStyle#bigLargeIcon(android.graphics.Bitmap)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_LARGE_ICON_BIG = EXTRA_LARGE_ICON + ".big"; + + /** + * {@link #getExtras extras} key: this is the progress value supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_PROGRESS = "android.progress"; + + /** + * {@link #getExtras extras} key: this is the maximum value supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_PROGRESS_MAX = "android.progressMax"; + + /** + * {@link #getExtras extras} key: whether the progress bar is indeterminate, supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate"; + + /** + * {@link #getExtras extras} key: whether the when field set using {@link Builder#setWhen} + * should be shown as a count-up timer (specifically a {@link android.widget.Chronometer}) + * instead of a timestamp, as supplied to {@link Builder#setUsesChronometer(boolean)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_SHOW_CHRONOMETER = "android.showChronometer"; + + /** + * {@link #getExtras extras} key: whether the chronometer set on the notification should count + * down instead of counting up. Is only relevant if key {@link #EXTRA_SHOW_CHRONOMETER} is + * present. This extra is a boolean. The default is (@code false). + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown"; + + /** + * {@link #getExtras extras} key: whether the notification should be colorized as + * supplied to {@link Builder#setColorized(boolean)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_COLORIZED = "android.colorized"; + + /** + * {@link #getExtras extras} key: whether the when field set using {@link Builder#setWhen} + * should be shown, as supplied to {@link Builder#setShowWhen(boolean)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_SHOW_WHEN = "android.showWhen"; + + /** + * {@link #getExtras extras} key: this is a bitmap to be shown in {@link BigPictureStyle} + * expanded notifications, supplied to + * {@link BigPictureStyle#bigPicture(android.graphics.Bitmap)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_PICTURE = "android.picture"; + + /** + * {@link #getExtras extras} key: this is an {@link Icon} of an image to be + * shown in {@link Notification.BigPictureStyle} expanded notifications, supplied to + * {@link BigPictureStyle#bigPicture(Icon)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_PICTURE_ICON = "android.pictureIcon"; + + /** + * {@link #getExtras extras} key: this is a content description of the big picture supplied from + * {@link BigPictureStyle#bigPicture(Bitmap)}, supplied to + * {@link BigPictureStyle#setContentDescription(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_PICTURE_CONTENT_DESCRIPTION = + "android.pictureContentDescription"; + + /** + * {@link #getExtras extras} key: this is a boolean to indicate that the + * {@link BigPictureStyle#bigPicture(Bitmap) big picture} is to be shown in the collapsed state + * of a {@link BigPictureStyle} notification. This will replace a + * {@link Builder#setLargeIcon(Bitmap) large icon} in that state if one was provided. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED = + "android.showBigPictureWhenCollapsed"; + + /** + * {@link #getExtras extras} key: An array of CharSequences to show in {@link InboxStyle} + * expanded notifications, each of which was supplied to + * {@link InboxStyle#addLine(CharSequence)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_TEXT_LINES = "android.textLines"; + + /** + * {@link #getExtras extras} key: A string representing the name of the specific + * {@link android.app.Notification.Style} used to create this notification. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_TEMPLATE = "android.template"; + + /** + * {@link #getExtras extras} key: A string representing the name of the specific + * {@link NotificationCompat.Style} used to create this notification. + */ + public static final String EXTRA_COMPAT_TEMPLATE = "androidx.core.app.extra.COMPAT_TEMPLATE"; + + /** + * {@link #getExtras extras} key: A String array containing the people that this + * notification relates to, each of which was supplied to + * {@link Builder#addPerson(String)}. + * + * @deprecated the actual objects are now in {@link #EXTRA_PEOPLE_LIST} + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + @Deprecated + public static final String EXTRA_PEOPLE = "android.people"; + + /** + * {@link #getExtras extras} key: : An arrayList of {@link Person} objects containing the + * people that this notification relates to, each of which was supplied to + * {@link Builder#addPerson(Person)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_PEOPLE_LIST = "android.people.list"; + + /** + * {@link #getExtras extras} key: A + * {@link android.content.ContentUris content URI} pointing to an image that can be displayed + * in the background when the notification is selected. The URI must point to an image stream + * suitable for passing into + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * BitmapFactory.decodeStream}; all other content types will be ignored. The content provider + * URI used for this purpose must require no permissions to read the image data. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; + + /** + * Notification key: A + * {@link android.media.session.MediaSession.Token} associated with a + * {@link android.app.Notification.MediaStyle} notification. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_MEDIA_SESSION = "android.mediaSession"; + + /** + * {@link #getExtras extras} key: the indices of actions to be shown in the compact view, + * as supplied to (e.g.) {@link Notification.MediaStyle#setShowActionsInCompactView(int...)}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions"; + + /** + * {@link #getExtras extras} key: the username to be displayed for all messages sent by the + * user including direct replies {@link MessagingStyle} notification. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_SELF_DISPLAY_NAME = "android.selfDisplayName"; + + /** + * {@link #getExtras extras} key: the person to display for all messages sent by the user, + * including direct replies to {@link MessagingStyle} notifications. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_MESSAGING_STYLE_USER = "android.messagingStyleUser"; + + /** + * {@link #getExtras extras} key: a {@link String} to be displayed as the title to a + * conversation represented by a {@link MessagingStyle}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle"; + + /** + * {@link #getExtras extras} key: an array of {@link MessagingStyle.Message} + * bundles provided by a {@link android.app.Notification.MessagingStyle} notification. + * This extra is a parcelable array of {@link Bundle} objects. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_MESSAGES = "android.messages"; + + /** + * {@link #getExtras extras} key: an array of {@link MessagingStyle#addHistoricMessage historic} + * {@link MessagingStyle.Message} bundles provided by a {@link MessagingStyle} notification. + * This extra is a parcelable array of {@link Bundle} objects. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic"; + + /** + * {@link #getExtras extras} key: whether the {@link NotificationCompat.MessagingStyle} + * notification represents a group conversation. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation"; + + /** + * {@link #getExtras extras} key: the type of call represented by the + * {@link android.app.Notification.CallStyle} notification. This extra is an int. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_CALL_TYPE = "android.callType"; + + /** + * {@link #getExtras extras} key: whether the {@link android.app.Notification.CallStyle} notification + * is for a call that will activate video when answered. This extra is a boolean. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_CALL_IS_VIDEO = "android.callIsVideo"; + + /** + * {@link #getExtras extras} key: the person to be displayed as calling for the + * {@link android.app.Notification.CallStyle} notification. This extra is a {@link Person}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_CALL_PERSON = "android.callPerson"; + + /** + * {@link #getExtras extras} key: the person to be displayed as calling for the + * {@link android.app.Notification.CallStyle} notification, for Android versions before the + * {@link Person} class was introduced. This extra is a {@link Bundle} representing a + * {@link Person}. + */ + @SuppressLint("ActionValue") + public static final String EXTRA_CALL_PERSON_COMPAT = "android.callPersonCompat"; + + /** + * {@link #getExtras extras} key: the icon to be displayed as a verification status of the + * caller on a {@link android.app.Notification.CallStyle} notification. This extra is an + * {@link Icon}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon"; + + /** + * {@link #getExtras extras} key: the icon to be displayed as a verification status of the + * caller on a {@link android.app.Notification.CallStyle} notification, for Android versions + * before the {@link Icon} class was introduced. This extra is an {@link Bundle} representing an + * {@link Icon}. + */ + @SuppressLint("ActionValue") + public static final String EXTRA_VERIFICATION_ICON_COMPAT = "android.verificationIconCompat"; + + /** + * {@link #getExtras extras} key: the text to be displayed as a verification status of the + * caller on a {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link CharSequence}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText"; + + /** + * {@link #getExtras extras} key: the intent to be sent when the users answers a + * {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link PendingIntent}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_ANSWER_INTENT = "android.answerIntent"; + + /** + * {@link #getExtras extras} key: the intent to be sent when the users declines a + * {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link PendingIntent}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_DECLINE_INTENT = "android.declineIntent"; + + /** + * {@link #getExtras extras} key: the intent to be sent when the users hangs up a + * {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link PendingIntent}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent"; + + /** + * {@link #getExtras extras} key: the color used as a hint for the Answer action button of a + * {@link android.app.Notification.CallStyle} notification. This extra is a {@link ColorInt}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_ANSWER_COLOR = "android.answerColor"; + + /** + * {@link #getExtras extras} key: the color used as a hint for the Decline or Hang Up action button of a + * {@link android.app.Notification.CallStyle} notification. This extra is a {@link ColorInt}. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_DECLINE_COLOR = "android.declineColor"; + + /** + * Key for compat's {@link MessagingStyle#getConversationTitle()}. This allows backwards support + * for conversation titles as SDK < P uses the title to denote group status. This hidden title + * doesn't appear in the notification shade. + */ + @SuppressLint("ActionValue") // Field & value copied from android.app.Notification + public static final String EXTRA_HIDDEN_CONVERSATION_TITLE = "android.hiddenConversationTitle"; + + /** + * Keys into the {@link #getExtras} Bundle: the audio contents of this notification. + * + * This is for use when rendering the notification on an audio-focused interface; + * the audio contents are a complete sound sample that contains the contents/body of the + * notification. This may be used in substitute of a Text-to-Speech reading of the + * notification. For example if the notification represents a voice message this should point + * to the audio of that message. + * + * The data stored under this key should be a String representation of a Uri that contains the + * audio contents in one of the following formats: WAV, PCM 16-bit, AMR-WB. + * + * This extra is unnecessary if you are using {@code MessagingStyle} since each {@code Message} + * has a field for holding data URI. That field can be used for audio. + * See {@code Message#setData}. + * + * Example usage: + *
+ * {@code
+ * NotificationCompat.Builder myBuilder = (build your Notification as normal);
+ * myBuilder.getExtras().putString(EXTRA_AUDIO_CONTENTS_URI, myAudioUri.toString());
+ * }
+ *
+ */
+ @SuppressLint("ActionValue") // Field & value copied from android.app.Notification
+ public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
+
+ /**
+ * Value of {@link Notification#color} equal to 0 (also known as
+ * {@link android.graphics.Color#TRANSPARENT Color.TRANSPARENT}),
+ * telling the system not to decorate this notification with any special color but instead use
+ * default colors when presenting this notification.
+ */
+ @ColorInt
+ public static final int COLOR_DEFAULT = Color.TRANSPARENT;
+
+ /**
+ * Maximum number of (generic) action buttons in a notification (contextual action buttons are
+ * handled separately).
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public static final int MAX_ACTION_BUTTONS = 3;
+
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @IntDef({AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_SYSTEM, AudioManager.STREAM_RING,
+ AudioManager.STREAM_MUSIC, AudioManager.STREAM_ALARM, AudioManager.STREAM_NOTIFICATION,
+ AudioManager.STREAM_DTMF, AudioManager.STREAM_ACCESSIBILITY})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StreamType {}
+
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Retention(SOURCE)
+ @IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET})
+ public @interface NotificationVisibility {}
+ /**
+ * Notification visibility: Show this notification in its entirety on all lockscreens.
+ *
+ * {@see android.app.Notification#visibility}
+ */
+ public static final int VISIBILITY_PUBLIC = Notification.VISIBILITY_PUBLIC;
+
+ /**
+ * Notification visibility: Show this notification on all lockscreens, but conceal sensitive or
+ * private information on secure lockscreens.
+ *
+ * {@see android.app.Notification#visibility}
+ */
+ public static final int VISIBILITY_PRIVATE = Notification.VISIBILITY_PRIVATE;
+
+ /**
+ * Notification visibility: Do not reveal any part of this notification on a secure lockscreen.
+ *
+ * {@see android.app.Notification#visibility}
+ */
+ public static final int VISIBILITY_SECRET = Notification.VISIBILITY_SECRET;
+
+ /**
+ * Notification category: incoming call (voice or video) or similar synchronous communication request.
+ */
+ public static final String CATEGORY_CALL = Notification.CATEGORY_CALL;
+
+ /**
+ * Notification category: map turn-by-turn navigation.
+ */
+ public static final String CATEGORY_NAVIGATION = Notification.CATEGORY_NAVIGATION;
+
+ /**
+ * Notification category: incoming direct message (SMS, instant message, etc.).
+ */
+ public static final String CATEGORY_MESSAGE = Notification.CATEGORY_MESSAGE;
+
+ /**
+ * Notification category: asynchronous bulk message (email).
+ */
+ public static final String CATEGORY_EMAIL = Notification.CATEGORY_EMAIL;
+
+ /**
+ * Notification category: calendar event.
+ */
+ public static final String CATEGORY_EVENT = Notification.CATEGORY_EVENT;
+
+ /**
+ * Notification category: promotion or advertisement.
+ */
+ public static final String CATEGORY_PROMO = Notification.CATEGORY_PROMO;
+
+ /**
+ * Notification category: alarm or timer.
+ */
+ public static final String CATEGORY_ALARM = Notification.CATEGORY_ALARM;
+
+ /**
+ * Notification category: progress of a long-running background operation.
+ */
+ public static final String CATEGORY_PROGRESS = Notification.CATEGORY_PROGRESS;
+
+ /**
+ * Notification category: social network or sharing update.
+ */
+ public static final String CATEGORY_SOCIAL = Notification.CATEGORY_SOCIAL;
+
+ /**
+ * Notification category: error in background operation or authentication status.
+ */
+ public static final String CATEGORY_ERROR = Notification.CATEGORY_ERROR;
+
+ /**
+ * Notification category: media transport control for playback.
+ */
+ public static final String CATEGORY_TRANSPORT = Notification.CATEGORY_TRANSPORT;
+
+ /**
+ * Notification category: system or device status update. Reserved for system use.
+ */
+ public static final String CATEGORY_SYSTEM = Notification.CATEGORY_SYSTEM;
+
+ /**
+ * Notification category: indication of running background service.
+ */
+ public static final String CATEGORY_SERVICE = Notification.CATEGORY_SERVICE;
+
+ /**
+ * Notification category: user-scheduled reminder.
+ */
+ public static final String CATEGORY_REMINDER = Notification.CATEGORY_REMINDER;
+
+ /**
+ * Notification category: a specific, timely recommendation for a single thing.
+ * For example, a news app might want to recommend a news story it believes the user will
+ * want to read next.
+ */
+ public static final String CATEGORY_RECOMMENDATION =
+ Notification.CATEGORY_RECOMMENDATION;
+
+ /**
+ * Notification category: ongoing information about device or contextual status.
+ */
+ public static final String CATEGORY_STATUS = Notification.CATEGORY_STATUS;
+
+ /**
+ * Notification category: tracking a user's workout.
+ */
+ public static final String CATEGORY_WORKOUT = "workout";
+
+ /**
+ * Notification category: temporarily sharing location.
+ */
+ public static final String CATEGORY_LOCATION_SHARING = "location_sharing";
+
+ /**
+ * Notification category: running stopwatch.
+ */
+ public static final String CATEGORY_STOPWATCH = "stopwatch";
+
+ /**
+ * Notification category: missed call.
+ */
+ public static final String CATEGORY_MISSED_CALL = "missed_call";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @IntDef({BADGE_ICON_NONE, BADGE_ICON_SMALL, BADGE_ICON_LARGE})
+ public @interface BadgeIconType {}
+ /**
+ * If this notification is being shown as a badge, always show as a number.
+ */
+ public static final int BADGE_ICON_NONE = Notification.BADGE_ICON_NONE;
+
+ /**
+ * If this notification is being shown as a badge, use the icon provided to
+ * {@link Builder#setSmallIcon(int)} to represent this notification.
+ */
+ public static final int BADGE_ICON_SMALL = Notification.BADGE_ICON_SMALL;
+
+ /**
+ * If this notification is being shown as a badge, use the icon provided to
+ * {@link Builder#setLargeIcon(Bitmap)} to represent this notification.
+ */
+ public static final int BADGE_ICON_LARGE = Notification.BADGE_ICON_LARGE;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @IntDef({GROUP_ALERT_ALL, GROUP_ALERT_SUMMARY, GROUP_ALERT_CHILDREN})
+ public @interface GroupAlertBehavior {}
+
+ /**
+ * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all notifications in a
+ * group with sound or vibration ought to make sound or vibrate (respectively), so this
+ * notification will not be muted when it is in a group.
+ */
+ public static final int GROUP_ALERT_ALL = Notification.GROUP_ALERT_ALL;
+
+ /**
+ * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all children
+ * notification in a group should be silenced (no sound or vibration) even if they would
+ * otherwise make sound or vibrate. Use this constant to mute this notification if this
+ * notification is a group child. This must be applied to all children notifications you want
+ * to mute.
+ *
+ * For example, you might want to use this constant if you post a number of children + * notifications at once (say, after a periodic sync), and only need to notify the user + * audibly once. + */ + public static final int GROUP_ALERT_SUMMARY = Notification.GROUP_ALERT_SUMMARY; + + /** + * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that the summary + * notification in a group should be silenced (no sound or vibration) even if they would + * otherwise make sound or vibrate. Use this constant + * to mute this notification if this notification is a group summary. + * + *
For example, you might want to use this constant if only the children notifications + * in your group have content and the summary is only used to visually group notifications + * rather than to alert the user that new information is available. + */ + public static final int GROUP_ALERT_CHILDREN = Notification.GROUP_ALERT_CHILDREN; + + /** + * Constant for the {@link Builder#setGroup(String) group key} that's added to notifications + * that are not already grouped when {@link Builder#setNotificationSilent()} is used when + * {@link Build.VERSION#SDK_INT} is >= {@link Build.VERSION_CODES#O}. + */ + public static final String GROUP_KEY_SILENT = "silent"; + + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP_PREFIX) + @IntDef({FOREGROUND_SERVICE_DEFAULT, + FOREGROUND_SERVICE_IMMEDIATE, + FOREGROUND_SERVICE_DEFERRED}) + public @interface ServiceNotificationBehavior {} + + /** + * Constant for {@link Builder#setForegroundServiceBehavior(int)}. In Android 12 or later, + * if the Notification associated with starting a foreground service has been + * built using setForegroundServiceBehavior() with this behavior, display of + * the notification will often be suppressed for a short time to avoid visual + * disturbances to the user. + * + * @see NotificationCompat.Builder#setForegroundServiceBehavior(int) + * @see #FOREGROUND_SERVICE_IMMEDIATE + * @see #FOREGROUND_SERVICE_DEFERRED + */ + public static final int FOREGROUND_SERVICE_DEFAULT = + Notification.FOREGROUND_SERVICE_DEFAULT; + + /** + * Constant for {@link Builder#setForegroundServiceBehavior(int)}. In Android 12 or later, + * if the Notification associated with starting a foreground service has been + * built using setForegroundServiceBehavior() with this behavior, display of + * the notification will be immediate even if the default behavior would be + * to defer visibility for a short time. + * + * @see NotificationCompat.Builder#setForegroundServiceBehavior(int) + * @see #FOREGROUND_SERVICE_DEFAULT + * @see #FOREGROUND_SERVICE_DEFERRED + */ + public static final int FOREGROUND_SERVICE_IMMEDIATE = + Notification.FOREGROUND_SERVICE_IMMEDIATE; + + /** + * Constant for {@link Builder#setForegroundServiceBehavior(int)}. In Android 12 or later, + * if the Notification associated with starting a foreground service has been + * built using setForegroundServiceBehavior() with this behavior, display of + * the notification will usually be suppressed for a short time to avoid visual + * disturbances to the user. + * + * @see NotificationCompat.Builder#setForegroundServiceBehavior(int) + * @see #FOREGROUND_SERVICE_DEFAULT + * @see #FOREGROUND_SERVICE_IMMEDIATE + */ + public static final int FOREGROUND_SERVICE_DEFERRED = + Notification.FOREGROUND_SERVICE_DEFERRED; + + /** + * Builder class for {@link NotificationCompat} objects. Allows easier control over + * all the flags, as well as help constructing the typical notification layouts. + *
+ * On platform versions that don't offer expanded notifications, methods that depend on + * expanded notifications have no effect. + *
+ *+ * For example, action buttons won't appear on platforms prior to Android 4.1. Action + * buttons depend on expanded notifications, which are only available in Android 4.1 + * and later. + *
+ * For this reason, you should always ensure that UI controls in a notification are also + * available in an {@link android.app.Activity} in your app, and you should always start that + * {@link android.app.Activity} when users click the notification. To do this, use the + * {@link NotificationCompat.Builder#setContentIntent setContentIntent()} + * method. + *
+ * + */ + public static class Builder { + /** + * Maximum length of CharSequences accepted by Builder and friends. + * + *
+ * Avoids spamming the system with overly large strings such as full e-mails.
+ */
+ private static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024;
+
+ // All these variables are declared public/hidden so they can be accessed by a builder
+ // extender.
+
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public Context mContext;
+
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public ArrayList This information should only be provided if it provides an essential
+ * benefit to the understanding of the notification. The more text you provide the
+ * less readable it becomes. For example, an email client should only provide the account
+ * name here if more than one email account has been added. As of {@link android.os.Build.VERSION_CODES#N} this information is displayed in the
+ * notification header area. On Android versions before {@link android.os.Build.VERSION_CODES#N}
+ * this will be shown in the third line of text in the platform notification template.
+ * You should not be using {@link #setProgress(int, int, boolean)} at the
+ * same time on those versions; they occupy the same place.
+ * This text does not appear within notification {@link Style templates} but may
+ * appear when the user uses an affordance to learn more about the notification.
+ * Additionally, this text will not appear unless you provide a valid link target by
+ * handling {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}.
+ *
+ * This text is meant to be concise description about what the user can customize
+ * when they click on this link. The recommended maximum length is 40 characters.
+ *
+ * Prior to {@link Build.VERSION_CODES#O} this field has no effect.
+ */
+ @NonNull
+ public Builder setSettingsText(@Nullable CharSequence text) {
+ mSettingsText = limitCharSequenceLength(text);
+ return this;
+ }
+
+ /**
+ * Set the remote input history.
+ *
+ * This should be set to the most recent inputs that have been sent
+ * through a {@link RemoteInput} of this Notification and cleared once the it is no
+ * longer relevant (e.g. for chat notifications once the other party has responded).
+ *
+ * The most recent input must be stored at the 0 index, the second most recent at the
+ * 1 index, etc. Note that the system will limit both how far back the inputs will be shown
+ * and how much of each individual input is shown.
+ *
+ * Note: The reply text will only be shown on notifications that have least one action
+ * with a {@code RemoteInput}.
+ * On some platforms, the system UI may choose to display a heads-up notification,
+ * instead of launching this intent, while the user is using the device.
+ *
+ * On some platforms, a notification that is noisy is more likely to be presented
+ * as a heads-up notification.
+ * On platforms {@link Build.VERSION_CODES#O} and above this value is ignored in favor
+ * of the value set on the {@link #setChannelId(String) notification's channel}. On older
+ * platforms, this value is still used, so it is still required for apps supporting
+ * those platforms.
+ * On some platforms, a notification that is noisy is more likely to be presented
+ * as a heads-up notification.
+ * On platforms {@link Build.VERSION_CODES#O} and above this value is ignored in favor
+ * of the value set on the {@link #setChannelId(String) notification's channel}. On older
+ * platforms, this value is still used, so it is still required for apps supporting
+ * those platforms.
+ * On some platforms, a notification that vibrates is more likely to be presented
+ * as a heads-up notification.
+ * On platforms {@link Build.VERSION_CODES#O} and above this value is ignored in favor
+ * of the value set on the {@link #setChannelId(String) notification's channel}. On older
+ * platforms, this value is still used, so it is still required for apps supporting
+ * those platforms. On platforms {@link Build.VERSION_CODES#O} and above this value is ignored in favor
+ * of the value set on the {@link #setChannelId(String) notification's channel}. On older
+ * platforms, this value is still used, so it is still required for apps supporting
+ * those platforms.
+ * This should only be used for high priority ongoing tasks like navigation, an ongoing
+ * call, or other similarly high-priority events for the user.
+ *
+ * For most styles, the coloring will only be applied if the notification is for a
+ * foreground service notification.
+ *
+ * However, for MediaStyle and DecoratedMediaCustomViewStyle notifications
+ * that have a media session attached there is no such requirement.
+ *
+ * Calling this method on any version prior to {@link android.os.Build.VERSION_CODES#O} will
+ * not have an effect on the notification and it won't be colorized.
+ *
+ * @see #setColor(int)
+ */
+ public @NonNull Builder setColorized(boolean colorize) {
+ mColorized = colorize;
+ mColorizedSet = true;
+ return this;
+ }
+
+ /**
+ * Set this flag if you would only like the sound, vibrate
+ * and ticker to be played if the notification is not already showing.
+ */
+ public @NonNull Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
+ setFlag(Notification.FLAG_ONLY_ALERT_ONCE, onlyAlertOnce);
+ return this;
+ }
+
+ /**
+ * Setting this flag will make it so the notification is automatically
+ * canceled when the user clicks it in the panel.
+ */
+ public @NonNull Builder setAutoCancel(boolean autoCancel) {
+ setFlag(Notification.FLAG_AUTO_CANCEL, autoCancel);
+ return this;
+ }
+
+ /**
+ * Set whether or not this notification is only relevant to the current device.
+ *
+ * Some notifications can be bridged to other devices for remote display.
+ * This hint can be set to recommend this notification not be bridged.
+ */
+ public @NonNull Builder setLocalOnly(boolean b) {
+ mLocalOnly = b;
+ return this;
+ }
+
+ /**
+ * Set the notification category.
+ *
+ * Must be one of the predefined notification categories (see the
+ * The value should be one or more of the following fields combined with
+ * bitwise-or:
+ * {@link Notification#DEFAULT_SOUND}, {@link Notification#DEFAULT_VIBRATE},
+ * {@link Notification#DEFAULT_LIGHTS}.
+ *
+ * For all default values, use {@link Notification#DEFAULT_ALL}.
+ *
+ * On platforms {@link Build.VERSION_CODES#O} and above this value is ignored in favor
+ * of the values set on the {@link #setChannelId(String) notification's channel}. On older
+ * platforms, this value is still used, so it is still required for apps supporting
+ * those platforms. Priority is an indication of how much of the user's valuable attention should be
+ * consumed by this notification. Low-priority notifications may be hidden from
+ * the user in certain situations, while the user might be interrupted for a
+ * higher-priority notification. The system sets a notification's priority based on
+ * various factors including the setPriority value. The effect may differ slightly on
+ * different platforms.
+ *
+ * On platforms {@link Build.VERSION_CODES#O} and above this value is ignored in favor
+ * of the importance value set on the {@link #setChannelId(String) notification's channel}.
+ * On older platforms, this value is still used, so it is still required for apps
+ * supporting those platforms.
+ * Depending on user preferences, this annotation may allow the notification to pass
+ * through interruption filters, and to appear more prominently in the user interface.
+ *
+ * The person should be specified by the {@code String} representation of a
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ * The system will also attempt to resolve {@code mailto:} and {@code tel:} schema
+ * URIs. The path part of these URIs must exist in the contacts database, in the
+ * appropriate column, or the reference will be discarded as invalid. Telephone schema
+ * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ *
+ * Depending on user preferences, this annotation may allow the notification to pass
+ * through interruption filters, if this notification is of category {@link #CATEGORY_CALL}
+ * or {@link #CATEGORY_MESSAGE}. The addition of people may also cause this notification to
+ * appear more prominently in the user interface.
+ *
+ * A person should usually contain a uri in order to benefit from the ranking boost.
+ * However, even if no uri is provided, it's beneficial to provide other people in the
+ * notification, such that listeners and voice only devices can announce and handle them
+ * properly.
+ * To make this notification the summary for its group, also call
+ * {@link #setGroupSummary}. A sort order can be specified for group members by using
+ * {@link #setSortKey}.
+ * @param groupKey The group key of the group.
+ * @return this object for method chaining
+ */
+ public @NonNull Builder setGroup(@Nullable String groupKey) {
+ mGroupKey = groupKey;
+ return this;
+ }
+
+ /**
+ * Set this notification to be the group summary for a group of notifications.
+ * Grouped notifications may display in a cluster or stack on devices which
+ * support such rendering. Requires a group key also be set using {@link #setGroup}.
+ * @param isGroupSummary Whether this notification should be a group summary.
+ * @return this object for method chaining
+ */
+ public @NonNull Builder setGroupSummary(boolean isGroupSummary) {
+ mGroupSummary = isGroupSummary;
+ return this;
+ }
+
+ /**
+ * Set a sort key that orders this notification among other notifications from the
+ * same package. This can be useful if an external sort was already applied and an app
+ * would like to preserve this. Notifications will be sorted lexicographically using this
+ * value, although providing different priorities in addition to providing sort key may
+ * cause this value to be ignored.
+ *
+ * This sort key can also be used to order members of a notification group. See
+ * {@link Builder#setGroup}.
+ *
+ * @see String#compareTo(String)
+ */
+ public @NonNull Builder setSortKey(@Nullable String sortKey) {
+ mSortKey = sortKey;
+ return this;
+ }
+
+ /**
+ * Merge additional metadata into this notification.
+ *
+ * Values within the Bundle will replace existing extras values in this Builder.
+ *
+ * @see Notification#extras
+ */
+ public @NonNull Builder addExtras(@Nullable Bundle extras) {
+ if (extras != null) {
+ if (mExtras == null) {
+ mExtras = new Bundle(extras);
+ } else {
+ mExtras.putAll(extras);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Set metadata for this notification.
+ *
+ * A reference to the Bundle is held for the lifetime of this Builder, and the Bundle's
+ * current contents are copied into the Notification each time {@link #build()} is
+ * called.
+ *
+ * Replaces any existing extras values with those from the provided Bundle.
+ * Use {@link #addExtras} to merge in metadata instead.
+ *
+ * @see Notification#extras
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Get the current metadata Bundle used by this notification Builder.
+ *
+ * The returned Bundle is shared with this Builder.
+ *
+ * The current contents of this Bundle are copied into the Notification each time
+ * {@link #build()} is called.
+ *
+ * @see Notification#extras
+ */
+ public @NonNull Bundle getExtras() {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ return mExtras;
+ }
+
+ /**
+ * Add an action to this notification. Actions are typically displayed by
+ * the system as a button adjacent to the notification content.
+ *
+ * For example, the following are some examples of notifications that belong in the
+ * conversation space:
+ * when as a timestamp, the notification will show an
+ * automatically updating display of the minutes and seconds since when.
+ *
+ * Useful when showing an elapsed time (like an ongoing phone call).
+ *
+ * @see android.widget.Chronometer
+ * @see Notification#when
+ */
+ public @NonNull Builder setUsesChronometer(boolean b) {
+ mUseChronometer = b;
+ return this;
+ }
+
+ /**
+ * Sets the Chronometer to count down instead of counting up.
+ *
+ * This is only relevant if setUsesChronometer(boolean) has been set to true. If it
+ * isn't set the chronometer will count up.
+ *
+ * @see android.widget.Chronometer
+ */
+ @RequiresApi(24)
+ public @NonNull Builder setChronometerCountDown(boolean countsDown) {
+ mChronometerCountDown = countsDown;
+ getExtras().putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, countsDown);
+ return this;
+ }
+
+ /**
+ * Set the small icon to use in the notification layouts. Different classes of devices
+ * may return different sizes. See the UX guidelines for more information on how to
+ * design these icons.
+ *
+ * @param icon A resource ID in the application's package of the drawable to use.
+ */
+ public @NonNull Builder setSmallIcon(int icon) {
+ mNotification.icon = icon;
+ return this;
+ }
+
+ /**
+ * A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional
+ * level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable
+ * LevelListDrawable}.
+ *
+ * @param icon A resource ID in the application's package of the drawable to use.
+ * @param level The level to use for the icon.
+ *
+ * @see android.graphics.drawable.LevelListDrawable
+ */
+ public @NonNull Builder setSmallIcon(int icon, int level) {
+ mNotification.icon = icon;
+ mNotification.iconLevel = level;
+ return this;
+ }
+
+ /**
+ * Silences this instance of the notification, regardless of the sounds or vibrations set
+ * on the notification or notification channel.
+ *
+ * @deprecated use {@link #setSilent(boolean)}
+ */
+ @Deprecated
+ public @NonNull Builder setNotificationSilent() {
+ mSilent = true;
+ return this;
+ }
+
+ /**
+ * If {@code true}, silences this instance of the notification, regardless of the sounds or
+ * vibrations set on the notification or notification channel. If {@code false}, then the
+ * normal sound and vibration logic applies. Defaults to {@code false}.
+ */
+ public @NonNull Builder setSilent(boolean silent) {
+ mSilent = silent;
+ return this;
+ }
+
+ /**
+ * Set the title (first row) of the notification, in a standard notification.
+ */
+ public @NonNull Builder setContentTitle(@Nullable CharSequence title) {
+ mContentTitle = limitCharSequenceLength(title);
+ return this;
+ }
+
+ /**
+ * Set the text (second row) of the notification, in a standard notification.
+ */
+ public @NonNull Builder setContentText(@Nullable CharSequence text) {
+ mContentText = limitCharSequenceLength(text);
+ return this;
+ }
+
+ /**
+ * This provides some additional information that is displayed in the notification. No
+ * guarantees are given where exactly it is displayed.
+ *
+ * STREAM_ constants.
+ */
+ public @NonNull Builder setSound(@Nullable Uri sound, @StreamType int streamType) {
+ mNotification.sound = sound;
+ mNotification.audioStreamType = streamType;
+ if (Build.VERSION.SDK_INT >= 21) {
+ AudioAttributes.Builder builder = Api21Impl.createBuilder();
+ builder = Api21Impl.setContentType(builder,
+ AudioAttributes.CONTENT_TYPE_SONIFICATION);
+ builder = Api21Impl.setLegacyStreamType(builder, streamType);
+ mNotification.audioAttributes = Api21Impl.build(builder);
+ }
+ return this;
+ }
+
+ /**
+ * Set the vibration pattern to use.
+ *
+ * pattern
+ * parameter.
+ */
+ public @NonNull Builder setVibrate(@Nullable long[] pattern) {
+ mNotification.vibrate = pattern;
+ return this;
+ }
+
+ /**
+ * Set the argb value that you would like the LED on the device to blink, as well as the
+ * rate. The rate is specified in terms of the number of milliseconds to be on
+ * and then the number of milliseconds to be off.
+ *
+ * CATEGORY_*
+ * constants in {@link Notification}) that best describes this notification.
+ * May be used by the system for ranking and filtering.
+ */
+ public @NonNull Builder setCategory(@Nullable String category) {
+ mCategory = category;
+ return this;
+ }
+
+ // TODO (b/149433438) support person field
+
+ /**
+ * Set the default notification options that will be used.
+ *
+ * Action buttons won't appear on platforms prior to Android 4.1. Action
+ * buttons depend on expanded notifications, which are only available in Android 4.1
+ * and later. To ensure that an action button's functionality is always available, first
+ * implement the functionality in the {@link android.app.Activity} that starts when a user
+ * clicks the notification (see {@link #setContentIntent setContentIntent()}), and then
+ * enhance the notification by implementing the same functionality with
+ * {@link #addAction addAction()}.
+ *
+ * @param icon Resource ID of a drawable that represents the action.
+ * @param title Text describing the action.
+ * @param intent {@link android.app.PendingIntent} to be fired when the action is invoked.
+ */
+ public @NonNull Builder addAction(int icon, @Nullable CharSequence title,
+ @Nullable PendingIntent intent) {
+ mActions.add(new Action(icon, title, intent));
+ return this;
+ }
+
+ /**
+ * Add an action to this notification. Actions are typically displayed by
+ * the system as a button adjacent to the notification content.
+ *
+ * Action buttons won't appear on platforms prior to Android 4.1. Action
+ * buttons depend on expanded notifications, which are only available in Android 4.1
+ * and later. To ensure that an action button's functionality is always available, first
+ * implement the functionality in the {@link android.app.Activity} that starts when a user
+ * clicks the notification (see {@link #setContentIntent setContentIntent()}), and then
+ * enhance the notification by implementing the same functionality with
+ * {@link #addAction addAction()}.
+ *
+ * @param action The action to add.
+ */
+ public @NonNull Builder addAction(@Nullable Action action) {
+ if (action != null) {
+ mActions.add(action);
+ }
+ return this;
+ }
+
+ /**
+ * Clear any actions added via {@link #addAction}
+ */
+ public @NonNull Builder clearActions() {
+ mActions.clear();
+ return this;
+ }
+
+ /**
+ * Add an invisible action to this notification. Invisible actions are never displayed by
+ * the system, but can be retrieved and used by other application listening to
+ * system notifications. Invisible actions are supported from Android 4.4.4 (API 20) and can
+ * be retrieved using {@link NotificationCompat#getInvisibleActions(Notification)}.
+ *
+ * @param icon Resource ID of a drawable that represents the action.
+ * @param title Text describing the action.
+ * @param intent {@link android.app.PendingIntent} to be fired when the action is invoked.
+ */
+ @RequiresApi(21)
+ @NonNull
+ public Builder addInvisibleAction(int icon, @Nullable CharSequence title,
+ @Nullable PendingIntent intent) {
+ mInvisibleActions.add(new Action(icon, title, intent));
+ return this;
+ }
+
+ /**
+ * Add an invisible action to this notification. Invisible actions are never displayed by
+ * the system, but can be retrieved and used by other application listening to
+ * system notifications. Invisible actions are supported from Android 4.4.4 (API 20) and can
+ * be retrieved using {@link NotificationCompat#getInvisibleActions(Notification)}.
+ *
+ * @param action The action to add.
+ */
+ @RequiresApi(21)
+ public @NonNull Builder addInvisibleAction(@Nullable Action action) {
+ if (action != null) {
+ mInvisibleActions.add(action);
+ }
+ return this;
+ }
+
+ /**
+ * Clear any invisible actions added via {@link #addInvisibleAction}
+ */
+ public @NonNull Builder clearInvisibleActions() {
+ mInvisibleActions.clear();
+ // NOTE: Building a notification actually mutates the extras on the builder, so we
+ // have to clear out those changes in order for the function to really work.
+ Bundle carExtenderBundle = mExtras.getBundle(CarExtender.EXTRA_CAR_EXTENDER);
+ if (carExtenderBundle != null) {
+ carExtenderBundle = new Bundle(carExtenderBundle);
+ carExtenderBundle.remove(CarExtender.EXTRA_INVISIBLE_ACTIONS);
+ mExtras.putBundle(CarExtender.EXTRA_CAR_EXTENDER, carExtenderBundle);
+ }
+ return this;
+ }
+
+ /**
+ * Add a rich notification style to be applied at build time.
+ *
+ * If the platform does not provide rich notification styles, this method has no effect. The
+ * user will always see the normal notification style.
+ *
+ * @param style Object responsible for modifying the notification style.
+ */
+ public @NonNull Builder setStyle(@Nullable Style style) {
+ if (mStyle != style) {
+ mStyle = style;
+ if (mStyle != null) {
+ mStyle.setBuilder(this);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets {@link Notification#color}.
+ *
+ * @param argb The accent color to use
+ *
+ * @return The same Builder.
+ */
+ public @NonNull Builder setColor(@ColorInt int argb) {
+ mColor = argb;
+ return this;
+ }
+
+ /**
+ * Sets {@link Notification#visibility}.
+ *
+ * @param visibility One of {@link Notification#VISIBILITY_PRIVATE} (the default),
+ * {@link Notification#VISIBILITY_PUBLIC}, or
+ * {@link Notification#VISIBILITY_SECRET}.
+ */
+ public @NonNull Builder setVisibility(@NotificationVisibility int visibility) {
+ mVisibility = visibility;
+ return this;
+ }
+
+ /**
+ * Supply a replacement Notification whose contents should be shown in insecure contexts
+ * (i.e. atop the secure lockscreen). See {@link Notification#visibility} and
+ * {@link #VISIBILITY_PUBLIC}.
+ *
+ * @param n A replacement notification, presumably with some or all info redacted.
+ * @return The same Builder.
+ */
+ public @NonNull Builder setPublicVersion(@Nullable Notification n) {
+ mPublicVersion = n;
+ return this;
+ }
+
+ /**
+ * @return whether the builder's createContentView() and peer methods should use the
+ * user-supplied RemoteViews. This will be true unless the style is a 'decorated' style,
+ * because those styles are defined to wrap that RemoteViews object.
+ */
+ private boolean useExistingRemoteView() {
+ return mStyle == null || !mStyle.displayCustomViewInline();
+ }
+
+ /**
+ * Construct a RemoteViews for the final notification layout.
+ */
+ @SuppressLint("BuilderSetStyle") // This API is copied from Notification.Builder
+ public @Nullable RemoteViews createContentView() {
+ // If the user setCustomContentView(), return it if appropriate for the style.
+ if (mContentView != null && useExistingRemoteView()) {
+ return mContentView;
+ }
+ // If there's a NotificationCompat.Style, and that Style produces a content view,
+ // then that content view is a backport of the Style's appearance to an old platform
+ // version, and it should be returned.
+ NotificationCompatBuilder compatBuilder = new NotificationCompatBuilder(this);
+ if (mStyle != null) {
+ RemoteViews styleView = mStyle.makeContentView(compatBuilder);
+ if (styleView != null) {
+ return styleView;
+ }
+ }
+ Notification notification = compatBuilder.build();
+ if (Build.VERSION.SDK_INT >= 24) {
+ // On N and newer, do some magic and delegate to the Platform's implementation
+ return Api24Impl.createContentView(Api24Impl.recoverBuilder(mContext,
+ notification));
+ } else {
+ // Before N, delegate to the deprecated field on the built notification
+ return notification.contentView;
+ }
+ }
+
+ /**
+ * Construct a RemoteViews for the final big notification layout.
+ */
+ @SuppressLint("BuilderSetStyle") // This API is copied from Notification.Builder
+ public @Nullable RemoteViews createBigContentView() {
+ // If the user setCustomBigContentView(), return it if appropriate for the style.
+ if (mBigContentView != null && useExistingRemoteView()) {
+ return mBigContentView;
+ }
+ // If there's a NotificationCompat.Style, and that Style produces a content view,
+ // then that content view is a backport of the Style's appearance to an old platform
+ // version, and it should be returned.
+ NotificationCompatBuilder compatBuilder = new NotificationCompatBuilder(this);
+ if (mStyle != null) {
+ RemoteViews styleView = mStyle.makeBigContentView(compatBuilder);
+ if (styleView != null) {
+ return styleView;
+ }
+ }
+ Notification notification = compatBuilder.build();
+ if (Build.VERSION.SDK_INT >= 24) {
+ // On N and newer, do some magic and delegate to the Platform's implementation
+ return Api24Impl.createBigContentView(Api24Impl.recoverBuilder(mContext,
+ notification));
+ } else {
+ // Before N, delegate to the deprecated field on the built notification
+ return notification.bigContentView;
+ }
+ }
+
+ /**
+ * Construct a RemoteViews for the final heads-up notification layout.
+ */
+ @SuppressLint("BuilderSetStyle") // This API is copied from Notification.Builder
+ public @Nullable RemoteViews createHeadsUpContentView() {
+ // Before Lollipop, there was no "heads up" notification view
+ if (Build.VERSION.SDK_INT < 21) {
+ return null;
+ }
+ // If the user setCustomHeadsUpContentView(), return it if appropriate for the style.
+ if (mHeadsUpContentView != null && useExistingRemoteView()) {
+ return mHeadsUpContentView;
+ }
+ // If there's a NotificationCompat.Style, and that Style produces a content view,
+ // then that content view is a backport of the Style's appearance to an old platform
+ // version, and it should be returned.
+ NotificationCompatBuilder compatBuilder = new NotificationCompatBuilder(this);
+ if (mStyle != null) {
+ RemoteViews styleView = mStyle.makeHeadsUpContentView(compatBuilder);
+ if (styleView != null) {
+ return styleView;
+ }
+ }
+ Notification notification = compatBuilder.build();
+ if (Build.VERSION.SDK_INT >= 24) {
+ // On N and newer, do some magic and delegate to the Platform's implementation
+ return Api24Impl.createHeadsUpContentView(Api24Impl.recoverBuilder(mContext,
+ notification));
+ } else {
+ // Before N, delegate to the deprecated field on the built notification
+ return notification.headsUpContentView;
+ }
+ }
+
+ /**
+ * Supply custom RemoteViews to use instead of the platform template.
+ *
+ * This will override the layout that would otherwise be constructed by this Builder
+ * object.
+ */
+ public @NonNull Builder setCustomContentView(@Nullable RemoteViews contentView) {
+ mContentView = contentView;
+ return this;
+ }
+
+ /**
+ * Supply custom RemoteViews to use instead of the platform template in the expanded form.
+ *
+ * This will override the expanded layout that would otherwise be constructed by this
+ * Builder object.
+ *
+ * No-op on versions prior to {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
+ */
+ public @NonNull Builder setCustomBigContentView(@Nullable RemoteViews contentView) {
+ mBigContentView = contentView;
+ return this;
+ }
+
+ /**
+ * Supply custom RemoteViews to use instead of the platform template in the heads up dialog.
+ *
+ * This will override the heads-up layout that would otherwise be constructed by this
+ * Builder object.
+ *
+ * No-op on versions prior to {@link android.os.Build.VERSION_CODES#LOLLIPOP}.
+ */
+ public @NonNull Builder setCustomHeadsUpContentView(@Nullable RemoteViews contentView) {
+ mHeadsUpContentView = contentView;
+ return this;
+ }
+
+ /**
+ * Specifies the channel the notification should be delivered on.
+ *
+ * No-op on versions prior to {@link android.os.Build.VERSION_CODES#O}.
+ */
+ public @NonNull Builder setChannelId(@NonNull String channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ /**
+ * Specifies the time at which this notification should be canceled, if it is not already
+ * canceled.
+ *
+ * No-op on versions prior to {@link android.os.Build.VERSION_CODES#O}.
+ */
+ public @NonNull Builder setTimeoutAfter(long durationMs) {
+ mTimeout = durationMs;
+ return this;
+ }
+
+ /**
+ * From Android 11, messaging notifications (those that use {@link MessagingStyle}) that
+ * use this method to link to a published long-lived sharing shortcut may appear in a
+ * dedicated Conversation section of the shade and may show configuration options that
+ * are unique to conversations. This behavior should be reserved for person to person(s)
+ * conversations where there is a likely social obligation for an individual to respond.
+ *
+ *
+ * And the following are some examples of notifications that do not belong in the
+ * conversation space:
+ *
+ *
+ *
+ * Additionally, this method can be used for all types of notifications to mark this + * notification as duplicative of a Launcher shortcut. Launchers that show badges or + * notification content may then suppress the shortcut in favor of the content of this + * notification. + *
+ * If this notification has {@link BubbleMetadata} attached that was created with + * a shortcutId a check will be performed to ensure the shortcutId supplied to bubble + * metadata matches the shortcutId set here, if one was set. If the shortcutId's were + * specified but do not match, an exception is thrown. + * + * @param shortcutId the {@link ShortcutInfoCompat#getId() id} of the shortcut this + * notification is linked to + * + * @see BubbleMetadata.Builder#Builder() + */ + public @NonNull Builder setShortcutId(@Nullable String shortcutId) { + mShortcutId = shortcutId; + return this; + } + + /** + * Populates this notification with given {@link ShortcutInfoCompat}. + * + *
Sets {@link androidx.core.content.pm.ShortcutInfoCompat#getId() shortcutId} based on + * the given shortcut. In addition, it also sets {@link LocusIdCompat locusId} and + * {@link #setContentTitle(CharSequence) contentTitle} if they were empty. + * + */ + public @NonNull Builder setShortcutInfo(@Nullable final ShortcutInfoCompat shortcutInfo) { + // TODO: b/156784300 add check to filter long-lived and sharing shortcut + if (shortcutInfo == null) { + return this; + } + mShortcutId = shortcutInfo.getId(); + if (mLocusId == null) { + if (shortcutInfo.getLocusId() != null) { + mLocusId = shortcutInfo.getLocusId(); + } else if (shortcutInfo.getId() != null) { + mLocusId = new LocusIdCompat(shortcutInfo.getId()); + } + } + if (mContentTitle == null) { + setContentTitle(shortcutInfo.getShortLabel()); + } + // TODO: b/156784300 include person info + return this; + } + + /** + * Sets the {@link LocusIdCompat} associated with this notification. + * + *
This method should be called when the {@link LocusIdCompat} is used in other places + * (such as {@link androidx.core.content.pm.ShortcutInfoCompat} and + * {@link android.view.contentcapture.ContentCaptureContext}) so the device's intelligence + * services can correlate them. + */ + public @NonNull Builder setLocusId(@Nullable final LocusIdCompat locusId) { + mLocusId = locusId; + return this; + } + + /** + * Sets which icon to display as a badge for this notification. + * + *
Must be one of {@link #BADGE_ICON_NONE}, {@link #BADGE_ICON_SMALL}, + * {@link #BADGE_ICON_LARGE}. + * + *
Note: This value might be ignored, for launchers that don't support + * badge icons. + */ + public @NonNull Builder setBadgeIconType(@BadgeIconType int icon) { + mBadgeIcon = icon; + return this; + } + + /** + * Sets the group alert behavior for this notification. Use this method to mute this + * notification if alerts for this notification's group should be handled by a different + * notification. This is only applicable for notifications that belong to a + * {@link #setGroup(String) group}. This must be called on all notifications you want to + * mute. For example, if you want only the summary of your group to make noise, all + * children in the group should have the group alert behavior {@link #GROUP_ALERT_SUMMARY}. + * + *
The default value is {@link #GROUP_ALERT_ALL}.
+ */ + public @NonNull Builder setGroupAlertBehavior(@GroupAlertBehavior int groupAlertBehavior) { + mGroupAlertBehavior = groupAlertBehavior; + return this; + } + + /** + * Specify a desired visibility policy for a Notification associated with a + * foreground service. The default value is {@link #FOREGROUND_SERVICE_DEFAULT}, + * meaning the system can choose to defer visibility of the notification for + * a short time after the service is started. Pass + * {@link NotificationCompat#FOREGROUND_SERVICE_IMMEDIATE FOREGROUND_SERVICE_IMMEDIATE} + * to this method in order to guarantee that visibility is never deferred. Pass + * {@link NotificationCompat#FOREGROUND_SERVICE_DEFERRED FOREGROUND_SERVICE_DEFERRED} + * to request that visibility is deferred whenever possible. + * + *Note that deferred visibility is not guaranteed. There + * may be some circumstances under which the system will show the foreground + * service's associated Notification immediately even when the app has used + * this method to explicitly request deferred display.
+ * + * This method has no effect when running on versions prior to + * {@link android.os.Build.VERSION_CODES#S}. + */ + @SuppressWarnings("MissingGetterMatchingBuilder") // no underlying getter in platform API + @NonNull + public Builder setForegroundServiceBehavior(@ServiceNotificationBehavior int behavior) { + mFgsDeferBehavior = behavior; + return this; + } + + /** + * Sets the {@link BubbleMetadata} that will be used to display app content in a floating + * window over the existing foreground activity. + * + *This data will be ignored unless the notification is posted to a channel that + * allows {@link android.app.NotificationChannel#canBubble() bubbles}.
+ * + *Notifications allowed to bubble that have valid bubble metadata will display in + * collapsed state outside of the notification shade on unlocked devices. When a user + * interacts with the collapsed state, the bubble intent will be invoked and displayed.
+ */ + public @NonNull Builder setBubbleMetadata(@Nullable BubbleMetadata data) { + mBubbleMetadata = data; + return this; + } + + /** + * Apply an extender to this notification builder. Extenders may be used to add + * metadata or change options on this builder. + */ + public @NonNull Builder extend(@NonNull Extender extender) { + extender.extend(this); + return this; + } + + /** + * Determines whether the platform can generate contextual actions for a notification. + * By default this is true. + */ + public @NonNull Builder setAllowSystemGeneratedContextualActions(boolean allowed) { + mAllowSystemGeneratedContextualActions = allowed; + return this; + } + + /** + * @deprecated Use {@link #build()} instead. + */ + @Deprecated + public @NonNull Notification getNotification() { + return build(); + } + + /** + * Combine all of the options that have been set and return a new {@link Notification} + * object. + */ + public @NonNull Notification build() { + return new NotificationCompatBuilder(this).build(); + } + + protected static @Nullable CharSequence limitCharSequenceLength(@Nullable CharSequence cs) { + if (cs == null) return cs; + if (cs.length() > MAX_CHARSEQUENCE_LENGTH) { + cs = cs.subSequence(0, MAX_CHARSEQUENCE_LENGTH); + } + return cs; + } + + /** + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public RemoteViews getContentView() { + return mContentView; + } + + /** + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public RemoteViews getBigContentView() { + return mBigContentView; + } + + /** + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public RemoteViews getHeadsUpContentView() { + return mHeadsUpContentView; + } + + /** + * return when if it is showing or 0 otherwise + * + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public long getWhenIfShowing() { + return mShowWhen ? mNotification.when : 0; + } + + /** + * @return the priority set on the notification + * + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public int getPriority() { + return mPriority; + } + + /** + * @return the foreground service behavior defined for the notification + * + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public int getForegroundServiceBehavior() { + return mFgsDeferBehavior; + } + + /** + * @return the color of the notification + * + */ + @ColorInt + @RestrictTo(LIBRARY_GROUP_PREFIX) + public int getColor() { + return mColor; + } + + /** + * @return the {@link BubbleMetadata} of the notification + * + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public @Nullable BubbleMetadata getBubbleMetadata() { + return mBubbleMetadata; + } + + /** + * A class for wrapping calls to {@link Notification.Builder} methods which + * were added in API 21; these calls must be wrapped to avoid performance issues. + * See the UnsafeNewApiCall lint rule for more details. + */ + @RequiresApi(21) + static class Api21Impl { + private Api21Impl() { } + + @DoNotInline + static AudioAttributes.Builder createBuilder() { + return new AudioAttributes.Builder(); + } + + @DoNotInline + static AudioAttributes.Builder setContentType(AudioAttributes.Builder builder, + int contentType) { + return builder.setContentType(contentType); + } + + @DoNotInline + static AudioAttributes.Builder setUsage(AudioAttributes.Builder builder, int usage) { + return builder.setUsage(usage); + } + + @DoNotInline + static AudioAttributes.Builder setLegacyStreamType(AudioAttributes.Builder builder, + int streamType) { + return builder.setLegacyStreamType(streamType); + } + + @DoNotInline + static AudioAttributes build(AudioAttributes.Builder builder) { + return builder.build(); + } + } + + /** + * A class for wrapping calls to {@link Notification.Builder} methods which + * were added in API 23; these calls must be wrapped to avoid performance issues. + * See the UnsafeNewApiCall lint rule for more details. + */ + @RequiresApi(23) + static class Api23Impl { + private Api23Impl() { } + + @DoNotInline + static Icon getSmallIcon(Notification notification) { + return notification.getSmallIcon(); + } + + @DoNotInline + static Icon getLargeIcon(Notification notification) { + return notification.getLargeIcon(); + } + } + + /** + * A class for wrapping calls to {@link Notification.Builder} methods which + * were added in API 24; these calls must be wrapped to avoid performance issues. + * See the UnsafeNewApiCall lint rule for more details. + */ + @RequiresApi(24) + static class Api24Impl { + private Api24Impl() { } + + @DoNotInline + static Notification.Builder recoverBuilder(Context context, Notification n) { + return Notification.Builder.recoverBuilder(context, n); + } + + @DoNotInline + static RemoteViews createContentView(Notification.Builder builder) { + return builder.createContentView(); + } + + @DoNotInline + static RemoteViews createHeadsUpContentView(Notification.Builder builder) { + return builder.createHeadsUpContentView(); + } + + @DoNotInline + static RemoteViews createBigContentView(Notification.Builder builder) { + return builder.createHeadsUpContentView(); + } + } + } + + /** + * An object that can apply a rich notification style to a {@link Notification.Builder} + * object. + *apply() has been called. This means
+ * that you only need to add data which won't be populated by the framework Notification
+ * which was built so far.
+ *
+ * Moreover, recovering builders and styles is only supported at API 19 and above, no
+ * implementation is required for current BigTextStyle, BigPictureStyle, or InboxStyle.
+ *
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public void addCompatExtras(@NonNull Bundle extras) {
+ if (mSummaryTextSet) {
+ extras.putCharSequence(EXTRA_SUMMARY_TEXT, mSummaryText);
+ }
+ if (mBigContentTitle != null) {
+ extras.putCharSequence(EXTRA_TITLE_BIG, mBigContentTitle);
+ }
+ String className = getClassName();
+ if (className != null) {
+ extras.putString(EXTRA_COMPAT_TEMPLATE, className);
+ }
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ protected void restoreFromCompatExtras(@NonNull Bundle extras) {
+ if (extras.containsKey(EXTRA_SUMMARY_TEXT)) {
+ mSummaryText = extras.getCharSequence(EXTRA_SUMMARY_TEXT);
+ mSummaryTextSet = true;
+ }
+ mBigContentTitle = extras.getCharSequence(EXTRA_TITLE_BIG);
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ protected void clearCompatExtraKeys(@NonNull Bundle extras) {
+ extras.remove(EXTRA_SUMMARY_TEXT);
+ extras.remove(EXTRA_TITLE_BIG);
+ extras.remove(EXTRA_COMPAT_TEMPLATE);
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Nullable
+ public static Style extractStyleFromNotification(@NonNull Notification notification) {
+ Bundle extras = NotificationCompat.getExtras(notification);
+ if (extras == null) {
+ return null;
+ }
+ return constructStyleForExtras(extras);
+ }
+
+ @Nullable
+ private static Style constructCompatStyleByPlatformName(
+ @Nullable String platformTemplateClass) {
+ if (platformTemplateClass == null) {
+ return null;
+ }
+ if (platformTemplateClass.equals(Notification.BigPictureStyle.class.getName())) {
+ return new BigPictureStyle();
+ }
+ if (platformTemplateClass.equals(Notification.BigTextStyle.class.getName())) {
+ return new BigTextStyle();
+ }
+ if (platformTemplateClass.equals(Notification.InboxStyle.class.getName())) {
+ return new InboxStyle();
+ }
+ if (Build.VERSION.SDK_INT >= 24) {
+ if (platformTemplateClass.equals(Notification.MessagingStyle.class.getName())) {
+ return new MessagingStyle();
+ }
+ if (platformTemplateClass.equals(
+ Notification.DecoratedCustomViewStyle.class.getName())) {
+ return new DecoratedCustomViewStyle();
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ static Style constructCompatStyleByName(@Nullable String templateClass) {
+ if (templateClass != null) {
+ switch (templateClass) {
+ case BigTextStyle.TEMPLATE_CLASS_NAME:
+ return new BigTextStyle();
+ case BigPictureStyle.TEMPLATE_CLASS_NAME:
+ return new BigPictureStyle();
+ case InboxStyle.TEMPLATE_CLASS_NAME:
+ return new InboxStyle();
+ case DecoratedCustomViewStyle.TEMPLATE_CLASS_NAME:
+ return new DecoratedCustomViewStyle();
+ case MessagingStyle.TEMPLATE_CLASS_NAME:
+ return new MessagingStyle();
+ case CallStyle.TEMPLATE_CLASS_NAME:
+ return new CallStyle();
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ static Style constructCompatStyleForBundle(@NonNull Bundle extras) {
+ // If the compat template name provided in the bundle can be resolved to a class, use
+ // that style class.
+ Style style = constructCompatStyleByName(extras.getString(EXTRA_COMPAT_TEMPLATE));
+ if (style != null) {
+ return style;
+ }
+ // Check for some specific extras which indicate the particular style that was used.
+ // Start with MessagingStyle which this library (since before EXTRA_COMPAT_TEMPLATE)
+ // creates as Notification.BigTextStyle, and thus both fields are contained.
+ if (extras.containsKey(EXTRA_SELF_DISPLAY_NAME)
+ || extras.containsKey(EXTRA_MESSAGING_STYLE_USER)) {
+ return new MessagingStyle();
+ } else if (extras.containsKey(EXTRA_PICTURE)
+ || extras.containsKey(EXTRA_PICTURE_ICON)) {
+ return new BigPictureStyle();
+ } else if (extras.containsKey(EXTRA_BIG_TEXT)) {
+ return new BigTextStyle();
+ } else if (extras.containsKey(EXTRA_TEXT_LINES)) {
+ return new InboxStyle();
+ } else if (extras.containsKey(EXTRA_CALL_TYPE)) {
+ return new CallStyle();
+ }
+ // If individual extras do not help identify the style, use the framework style name.
+ return constructCompatStyleByPlatformName(extras.getString(EXTRA_TEMPLATE));
+ }
+
+ @Nullable
+ static Style constructStyleForExtras(@NonNull Bundle extras) {
+ final Style style = constructCompatStyleForBundle(extras);
+ if (style == null) {
+ return null;
+ }
+ try {
+ style.restoreFromCompatExtras(extras);
+ return style;
+ } catch (ClassCastException e) {
+ return null;
+ }
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @NonNull
+ public RemoteViews applyStandardTemplate(boolean showSmallIcon,
+ int resId, boolean fitIn1U) {
+ Resources res = mBuilder.mContext.getResources();
+ RemoteViews contentView = new RemoteViews(mBuilder.mContext.getPackageName(), resId);
+ boolean showLine3 = false;
+ boolean showLine2 = false;
+
+ boolean minPriority = mBuilder.getPriority() < NotificationCompat.PRIORITY_LOW;
+ if (Build.VERSION.SDK_INT < 21) {
+ // lets color the backgrounds
+ if (minPriority) {
+ contentView.setInt(R.id.notification_background,
+ "setBackgroundResource", R.drawable.notification_bg_low);
+ contentView.setInt(R.id.icon,
+ "setBackgroundResource", R.drawable.notification_template_icon_low_bg);
+ } else {
+ contentView.setInt(R.id.notification_background,
+ "setBackgroundResource", R.drawable.notification_bg);
+ contentView.setInt(R.id.icon,
+ "setBackgroundResource", R.drawable.notification_template_icon_bg);
+ }
+ }
+
+ if (mBuilder.mLargeIcon != null) {
+ // On versions before Jellybean, the large icon was shown by SystemUI, so we need
+ // to hide it here.
+ contentView.setViewVisibility(R.id.icon, View.VISIBLE);
+ contentView.setImageViewBitmap(R.id.icon,
+ createColoredBitmap(mBuilder.mLargeIcon, Color.TRANSPARENT));
+ if (showSmallIcon && mBuilder.mNotification.icon != 0) {
+ int backgroundSize = res.getDimensionPixelSize(
+ R.dimen.notification_right_icon_size);
+ int iconSize = backgroundSize - res.getDimensionPixelSize(
+ R.dimen.notification_small_icon_background_padding) * 2;
+ if (Build.VERSION.SDK_INT >= 21) {
+ Bitmap smallBit = createIconWithBackground(
+ mBuilder.mNotification.icon,
+ backgroundSize,
+ iconSize,
+ mBuilder.getColor());
+ contentView.setImageViewBitmap(R.id.right_icon, smallBit);
+ } else {
+ contentView.setImageViewBitmap(R.id.right_icon, createColoredBitmap(
+ mBuilder.mNotification.icon, Color.WHITE));
+ }
+ contentView.setViewVisibility(R.id.right_icon, View.VISIBLE);
+ }
+ } else if (showSmallIcon && mBuilder.mNotification.icon != 0) { // small icon at left
+ contentView.setViewVisibility(R.id.icon, View.VISIBLE);
+ if (Build.VERSION.SDK_INT >= 21) {
+ int backgroundSize = res.getDimensionPixelSize(
+ R.dimen.notification_large_icon_width)
+ - res.getDimensionPixelSize(R.dimen.notification_big_circle_margin);
+ int iconSize = res.getDimensionPixelSize(
+ R.dimen.notification_small_icon_size_as_large);
+ Bitmap smallBit = createIconWithBackground(
+ mBuilder.mNotification.icon,
+ backgroundSize,
+ iconSize,
+ mBuilder.getColor());
+ contentView.setImageViewBitmap(R.id.icon, smallBit);
+ } else {
+ contentView.setImageViewBitmap(R.id.icon, createColoredBitmap(
+ mBuilder.mNotification.icon, Color.WHITE));
+ }
+ }
+ if (mBuilder.mContentTitle != null) {
+ contentView.setTextViewText(R.id.title, mBuilder.mContentTitle);
+ }
+ if (mBuilder.mContentText != null) {
+ contentView.setTextViewText(R.id.text, mBuilder.mContentText);
+ showLine3 = true;
+ }
+ // If there is a large icon we have a right side
+ boolean hasRightSide =
+ !(Build.VERSION.SDK_INT >= 21) && mBuilder.mLargeIcon != null;
+ if (mBuilder.mContentInfo != null) {
+ contentView.setTextViewText(R.id.info, mBuilder.mContentInfo);
+ contentView.setViewVisibility(R.id.info, View.VISIBLE);
+ showLine3 = true;
+ hasRightSide = true;
+ } else if (mBuilder.mNumber > 0) {
+ final int tooBig = res.getInteger(
+ R.integer.status_bar_notification_info_maxnum);
+ if (mBuilder.mNumber > tooBig) {
+ contentView.setTextViewText(R.id.info, ((Resources) res).getString(
+ R.string.status_bar_notification_info_overflow));
+ } else {
+ NumberFormat f = NumberFormat.getIntegerInstance();
+ contentView.setTextViewText(R.id.info, f.format(mBuilder.mNumber));
+ }
+ contentView.setViewVisibility(R.id.info, View.VISIBLE);
+ showLine3 = true;
+ hasRightSide = true;
+ } else {
+ contentView.setViewVisibility(R.id.info, View.GONE);
+ }
+
+ // Need to show three lines?
+ if (mBuilder.mSubText != null) {
+ contentView.setTextViewText(R.id.text, mBuilder.mSubText);
+ if (mBuilder.mContentText != null) {
+ contentView.setTextViewText(R.id.text2, mBuilder.mContentText);
+ contentView.setViewVisibility(R.id.text2, View.VISIBLE);
+ showLine2 = true;
+ } else {
+ contentView.setViewVisibility(R.id.text2, View.GONE);
+ }
+ }
+
+ // RemoteViews.setViewPadding and RemoteViews.setTextViewTextSize is not available on
+ // ICS-
+ if (showLine2) {
+ if (fitIn1U) {
+ // need to shrink all the type to make sure everything fits
+ final float subTextSize = res.getDimensionPixelSize(
+ R.dimen.notification_subtext_size);
+ contentView.setTextViewTextSize(R.id.text, TypedValue.COMPLEX_UNIT_PX,
+ subTextSize);
+ }
+ // vertical centering
+ contentView.setViewPadding(R.id.line1, 0, 0, 0, 0);
+ }
+
+ if (mBuilder.getWhenIfShowing() != 0) {
+ if (mBuilder.mUseChronometer) {
+ contentView.setViewVisibility(R.id.chronometer, View.VISIBLE);
+ contentView.setLong(R.id.chronometer, "setBase",
+ mBuilder.getWhenIfShowing()
+ + (SystemClock.elapsedRealtime() - System.currentTimeMillis()));
+ contentView.setBoolean(R.id.chronometer, "setStarted", true);
+ if (mBuilder.mChronometerCountDown && Build.VERSION.SDK_INT >= 24) {
+ Api24Impl.setChronometerCountDown(contentView, R.id.chronometer,
+ mBuilder.mChronometerCountDown);
+ }
+ } else {
+ contentView.setViewVisibility(R.id.time, View.VISIBLE);
+ contentView.setLong(R.id.time, "setTime", mBuilder.getWhenIfShowing());
+ }
+ hasRightSide = true;
+ }
+ contentView.setViewVisibility(R.id.right_side, hasRightSide ? View.VISIBLE : View.GONE);
+ contentView.setViewVisibility(R.id.line3, showLine3 ? View.VISIBLE : View.GONE);
+ return contentView;
+ }
+
+ /**
+ * Create a bitmap using the given icon together with a color filter created from the given
+ * color.
+ *
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public Bitmap createColoredBitmap(int iconId, int color) {
+ return createColoredBitmap(iconId, color, 0 /* size */);
+ }
+
+ /**
+ * Create a bitmap using the given icon together with a color filter created from the given
+ * color.
+ */
+ Bitmap createColoredBitmap(@NonNull IconCompat icon, int color) {
+ return createColoredBitmap(icon, color, 0 /* size */);
+ }
+
+ private Bitmap createColoredBitmap(int iconId, int color, int size) {
+ return createColoredBitmap(IconCompat.createWithResource(mBuilder.mContext, iconId),
+ color, size);
+ }
+
+ private Bitmap createColoredBitmap(@NonNull IconCompat icon, int color, int size) {
+ Drawable drawable = icon.loadDrawable(mBuilder.mContext);
+ int width = size == 0 ? drawable.getIntrinsicWidth() : size;
+ int height = size == 0 ? drawable.getIntrinsicHeight() : size;
+ Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ drawable.setBounds(0, 0, width, height);
+ if (color != 0) {
+ drawable.mutate().setColorFilter(
+ new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
+ }
+ Canvas canvas = new Canvas(resultBitmap);
+ drawable.draw(canvas);
+ return resultBitmap;
+ }
+
+ private Bitmap createIconWithBackground(int iconId, int size,
+ int iconSize, int color) {
+ Bitmap coloredBitmap = createColoredBitmap(R.drawable.notification_icon_background,
+ color == NotificationCompat.COLOR_DEFAULT ? 0 : color, size);
+ Canvas canvas = new Canvas(coloredBitmap);
+ Drawable icon = mBuilder.mContext.getResources().getDrawable(iconId).mutate();
+ icon.setFilterBitmap(true);
+ int inset = (size - iconSize) / 2;
+ icon.setBounds(inset, inset, iconSize + inset, iconSize + inset);
+ icon.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP));
+ icon.draw(canvas);
+ return coloredBitmap;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public void buildIntoRemoteViews(RemoteViews outerView,
+ RemoteViews innerView) {
+ // this needs to be done fore the other calls, since otherwise we might hide the wrong
+ // things if our ids collide.
+ hideNormalContent(outerView);
+ outerView.removeAllViews(R.id.notification_main_column);
+ outerView.addView(R.id.notification_main_column, innerView.clone());
+ outerView.setViewVisibility(R.id.notification_main_column, View.VISIBLE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ // Adjust padding depending on font size.
+ int top = calculateTopPadding();
+ outerView.setViewPadding(R.id.notification_main_column_container, 0, top, 0, 0);
+ }
+ }
+
+ private void hideNormalContent(RemoteViews outerView) {
+ outerView.setViewVisibility(R.id.title, View.GONE);
+ outerView.setViewVisibility(R.id.text2, View.GONE);
+ outerView.setViewVisibility(R.id.text, View.GONE);
+ }
+
+ private int calculateTopPadding() {
+ Resources resources = mBuilder.mContext.getResources();
+ int padding = resources.getDimensionPixelSize(R.dimen.notification_top_pad);
+ int largePadding = resources.getDimensionPixelSize(
+ R.dimen.notification_top_pad_large_text);
+ float fontScale = resources.getConfiguration().fontScale;
+ float largeFactor = (constrain(fontScale, 1.0f, 1.3f) - 1f) / (1.3f - 1f);
+
+ // Linearly interpolate the padding between large and normal with the font scale ranging
+ // from 1f to LARGE_TEXT_SCALE
+ return Math.round((1 - largeFactor) * padding + largeFactor * largePadding);
+ }
+
+ private static float constrain(float amount, float low, float high) {
+ return amount < low ? low : (amount > high ? high : amount);
+ }
+
+ /**
+ * A class for wrapping calls to {@link NotificationCompat.Style} methods which
+ * were added in API 24; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(24)
+ static class Api24Impl {
+ private Api24Impl() { }
+
+ @DoNotInline
+ static void setChronometerCountDown(RemoteViews remoteViews, int viewId,
+ boolean isCountDown) {
+ remoteViews.setChronometerCountDown(viewId, isCountDown);
+ }
+
+ }
+ }
+
+ /**
+ * Helper class for generating large-format notifications that include a large image attachment.
+ *
+ * Notification notification = new NotificationCompat.Builder(mContext)
+ * .setContentTitle("New photo from " + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_post)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.BigPictureStyle()
+ * .bigPicture(aBigBitmap))
+ * .build();
+ *
+ *
+ * @see Notification#bigContentView
+ */
+ public static class BigPictureStyle extends Style {
+
+ private static final String TEMPLATE_CLASS_NAME =
+ "androidx.core.app.NotificationCompat$BigPictureStyle";
+
+ private IconCompat mPictureIcon;
+ private IconCompat mBigLargeIcon;
+ private boolean mBigLargeIconSet;
+ private CharSequence mPictureContentDescription;
+ private boolean mShowBigPictureWhenCollapsed;
+
+ public BigPictureStyle() {
+ }
+
+ public BigPictureStyle(@Nullable Builder builder) {
+ setBuilder(builder);
+ }
+
+ /**
+ * Overrides ContentTitle in the big form of the template.
+ * This defaults to the value passed to setContentTitle().
+ */
+ public @NonNull BigPictureStyle setBigContentTitle(@Nullable CharSequence title) {
+ mBigContentTitle = Builder.limitCharSequenceLength(title);
+ return this;
+ }
+
+ /**
+ * Set the first line of text after the detail section in the big form of the template.
+ */
+ public @NonNull BigPictureStyle setSummaryText(@Nullable CharSequence cs) {
+ mSummaryText = Builder.limitCharSequenceLength(cs);
+ mSummaryTextSet = true;
+ return this;
+ }
+
+ /**
+ * Set the content description of the big picture.
+ */
+ @RequiresApi(31)
+ @NonNull
+ public BigPictureStyle setContentDescription(
+ @Nullable CharSequence contentDescription) {
+ mPictureContentDescription = contentDescription;
+ return this;
+ }
+
+ /**
+ * Provide the bitmap to be used as the payload for the BigPicture notification.
+ */
+ public @NonNull BigPictureStyle bigPicture(@Nullable Bitmap b) {
+ mPictureIcon = b == null ? null : IconCompat.createWithBitmap(b);
+ return this;
+ }
+
+ /**
+ * Provide an icon to be used as the payload for the BigPicture notification.
+ * Note that certain features (like animated Icons) may not work on all versions.
+ */
+ @RequiresApi(31)
+ public @NonNull BigPictureStyle bigPicture(@Nullable Icon i) {
+ mPictureIcon = IconCompat.createFromIcon(i);
+ return this;
+ }
+
+ /**
+ * When set, the {@link #bigPicture(Bitmap) big picture} of this style will be promoted and
+ * shown in place of the {@link Builder#setLargeIcon(Bitmap) large icon} in the collapsed
+ * state of this notification.
+ */
+ @RequiresApi(31)
+ @NonNull
+ public BigPictureStyle showBigPictureWhenCollapsed(boolean show) {
+ mShowBigPictureWhenCollapsed = show;
+ return this;
+ }
+
+ /**
+ * Override the large icon when the big notification is shown.
+ */
+ public @NonNull BigPictureStyle bigLargeIcon(@Nullable Bitmap b) {
+ mBigLargeIcon = b == null ? null : IconCompat.createWithBitmap(b);
+ mBigLargeIconSet = true;
+ return this;
+ }
+
+ /**
+ * Override the large icon when the big notification is shown.
+ */
+ @RequiresApi(23)
+ public @NonNull BigPictureStyle bigLargeIcon(@Nullable Icon i) {
+ mBigLargeIcon = i == null ? null : IconCompat.createFromIcon(i);
+ mBigLargeIconSet = true;
+ return this;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @NonNull
+ protected String getClassName() {
+ return TEMPLATE_CLASS_NAME;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ Notification.Builder builder1 = builder.getBuilder();
+ Notification.BigPictureStyle bigPictureStyle =
+ new Notification.BigPictureStyle(builder1);
+ Notification.BigPictureStyle style =
+ bigPictureStyle.setBigContentTitle(mBigContentTitle);
+ if (mPictureIcon != null) {
+ // Attempts to set the icon for BigPictureStyle; prefers using data as Icon,
+ // with a fallback to store the Bitmap if Icon is not supported directly.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ Context context = null;
+ if (builder instanceof NotificationCompatBuilder) {
+ context = ((NotificationCompatBuilder) builder).getContext();
+ }
+ Api31Impl.setBigPicture(style, mPictureIcon.toIcon(context));
+ } else if (mPictureIcon.getType() == IconCompat.TYPE_BITMAP) {
+ Bitmap b = mPictureIcon.getBitmap();
+ style = style.bigPicture(b);
+ }
+ }
+ // Attempts to set the big large icon for BigPictureStyle.
+ if (mBigLargeIconSet) {
+ if (mBigLargeIcon == null) {
+ style.bigLargeIcon((Bitmap) null);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ Context context = null;
+ if (builder instanceof NotificationCompatBuilder) {
+ context = ((NotificationCompatBuilder) builder).getContext();
+ }
+ Api23Impl.setBigLargeIcon(style, mBigLargeIcon.toIcon(context));
+ } else if (mBigLargeIcon.getType() == IconCompat.TYPE_BITMAP) {
+ // Before M, only the Bitmap setter existed
+ style.bigLargeIcon(mBigLargeIcon.getBitmap());
+ } else {
+ // TODO(b/172282791): When we add #bigLargeIcon(Icon) we'll need to support
+ // other icon types here by rendering them into a new Bitmap.
+ style.bigLargeIcon((Bitmap) null);
+ }
+ }
+ if (mSummaryTextSet) {
+ style.setSummaryText(mSummaryText);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ Api31Impl.showBigPictureWhenCollapsed(style, mShowBigPictureWhenCollapsed);
+ Api31Impl.setContentDescription(style, mPictureContentDescription);
+ }
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @SuppressWarnings("deprecation")
+ protected void restoreFromCompatExtras(@NonNull Bundle extras) {
+ super.restoreFromCompatExtras(extras);
+
+ if (extras.containsKey(EXTRA_LARGE_ICON_BIG)) {
+ mBigLargeIcon = asIconCompat(extras.getParcelable(EXTRA_LARGE_ICON_BIG));
+ mBigLargeIconSet = true;
+ }
+ mPictureIcon = getPictureIcon(extras);
+ mShowBigPictureWhenCollapsed = extras.getBoolean(EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED);
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Nullable
+ public static IconCompat getPictureIcon(@Nullable Bundle extras) {
+ if (extras == null) return null;
+ // When this style adds a picture, we only add one of the keys. If both were added,
+ // it would most likely be a legacy app trying to override the picture in some way.
+ // Because of that case it's better to give precedence to the legacy field.
+ Parcelable bitmapPicture = extras.getParcelable(EXTRA_PICTURE);
+ if (bitmapPicture != null) {
+ return asIconCompat(bitmapPicture);
+ } else {
+ return asIconCompat(extras.getParcelable(EXTRA_PICTURE_ICON));
+ }
+ }
+
+ @Nullable
+ private static IconCompat asIconCompat(@Nullable Parcelable bitmapOrIcon) {
+ if (bitmapOrIcon != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (bitmapOrIcon instanceof Icon) {
+ return IconCompat.createFromIcon((Icon) bitmapOrIcon);
+ }
+ }
+ if (bitmapOrIcon instanceof Bitmap) {
+ return IconCompat.createWithBitmap((Bitmap) bitmapOrIcon);
+ }
+ }
+ return null;
+ }
+
+ /**
+ */
+ @Override
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ protected void clearCompatExtraKeys(@NonNull Bundle extras) {
+ super.clearCompatExtraKeys(extras);
+ extras.remove(EXTRA_LARGE_ICON_BIG);
+ extras.remove(EXTRA_PICTURE);
+ extras.remove(EXTRA_PICTURE_ICON);
+ extras.remove(EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED);
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification.BigPictureStyle} methods which
+ * were added in API 23; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(23)
+ private static class Api23Impl {
+ private Api23Impl() {
+ }
+
+ /**
+ * Calls {@link Notification.BigPictureStyle#bigLargeIcon(Icon)}
+ */
+ @RequiresApi(23)
+ static void setBigLargeIcon(Notification.BigPictureStyle style, Icon icon) {
+ style.bigLargeIcon(icon);
+ }
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification.BigPictureStyle} methods which
+ * were added in API 31; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(31)
+ private static class Api31Impl {
+ private Api31Impl() {
+ }
+
+ /**
+ * Calls {@link Notification.BigPictureStyle#showBigPictureWhenCollapsed(boolean)}
+ */
+ @RequiresApi(31)
+ static void showBigPictureWhenCollapsed(Notification.BigPictureStyle style,
+ boolean show) {
+ style.showBigPictureWhenCollapsed(show);
+ }
+
+ /**
+ * Calls {@link Notification.BigPictureStyle#setContentDescription(CharSequence)}
+ */
+ @RequiresApi(31)
+ static void setContentDescription(Notification.BigPictureStyle style,
+ CharSequence contentDescription) {
+ style.setContentDescription(contentDescription);
+ }
+
+ /**
+ * Calls {@link Notification.BigPictureStyle#bigPicture(Icon)}
+ */
+ @RequiresApi(31)
+ static void setBigPicture(Notification.BigPictureStyle style, Icon icon) {
+ style.bigPicture(icon);
+ }
+ }
+ }
+
+ /**
+ * Helper class for generating large-format notifications that include a lot of text.
+ *
+ *
+ * Notification notification = new Notification.Builder(mContext)
+ * .setContentTitle("New mail from " + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.BigTextStyle()
+ * .bigText(aVeryLongString))
+ * .build();
+ *
+ *
+ * @see Notification#bigContentView
+ */
+ public static class BigTextStyle extends Style {
+
+ private static final String TEMPLATE_CLASS_NAME =
+ "androidx.core.app.NotificationCompat$BigTextStyle";
+
+ private CharSequence mBigText;
+
+ public BigTextStyle() {
+ }
+
+ public BigTextStyle(@Nullable Builder builder) {
+ setBuilder(builder);
+ }
+
+ /**
+ * Overrides ContentTitle in the big form of the template.
+ * This defaults to the value passed to setContentTitle().
+ */
+ public @NonNull BigTextStyle setBigContentTitle(@Nullable CharSequence title) {
+ mBigContentTitle = Builder.limitCharSequenceLength(title);
+ return this;
+ }
+
+ /**
+ * Set the first line of text after the detail section in the big form of the template.
+ */
+ public @NonNull BigTextStyle setSummaryText(@Nullable CharSequence cs) {
+ mSummaryText = Builder.limitCharSequenceLength(cs);
+ mSummaryTextSet = true;
+ return this;
+ }
+
+ /**
+ * Provide the longer text to be displayed in the big form of the
+ * template in place of the content text.
+ */
+ public @NonNull BigTextStyle bigText(@Nullable CharSequence cs) {
+ mBigText = Builder.limitCharSequenceLength(cs);
+ return this;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @NonNull
+ protected String getClassName() {
+ return TEMPLATE_CLASS_NAME;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ Notification.Builder builder1 = builder.getBuilder();
+ Notification.BigTextStyle style =
+ new Notification.BigTextStyle(builder1);
+ style = style.setBigContentTitle(mBigContentTitle);
+ style = style.bigText(mBigText);
+ if (mSummaryTextSet) {
+ style.setSummaryText(mSummaryText);
+ }
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ protected void restoreFromCompatExtras(@NonNull Bundle extras) {
+ super.restoreFromCompatExtras(extras);
+
+ mBigText = extras.getCharSequence(EXTRA_BIG_TEXT);
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void addCompatExtras(@NonNull Bundle extras) {
+ super.addCompatExtras(extras);
+ // Reminder: this method only needs to add fields which are not added by the platform
+ // builder (and only needs to work at all for API 19+).
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ // On KitKat, BixTextStyle populated EXTRA_TEXT instead of EXTRA_BIG_TEXT, so this
+ // needs to populate EXTRA_BIG_TEXT to fix style recovery on that platform version.
+ extras.putCharSequence(EXTRA_BIG_TEXT, mBigText);
+ }
+ }
+
+ /**
+ */
+ @Override
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ protected void clearCompatExtraKeys(@NonNull Bundle extras) {
+ super.clearCompatExtraKeys(extras);
+ extras.remove(EXTRA_BIG_TEXT);
+ }
+ }
+
+ /**
+ * Helper class for generating large-format notifications that include multiple back-and-forth
+ * messages of varying types between any number of people.
+ *
+ *
+ *
+ * Notification notification = new Notification.Builder()
+ * .setContentTitle("2 new messages with " + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_message)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.MessagingStyle(resources.getString(R.string.reply_name))
+ * .addMessage(messages[0].getText(), messages[0].getTime(), messages[0].getSender())
+ * .addMessage(messages[1].getText(), messages[1].getTime(), messages[1].getSender()))
+ * .build();
+ *
+ */
+ public static class MessagingStyle extends Style {
+
+ private static final String TEMPLATE_CLASS_NAME =
+ "androidx.core.app.NotificationCompat$MessagingStyle";
+
+ /**
+ * The maximum number of messages that will be retained in the Notification itself (the
+ * number displayed is up to the platform).
+ */
+ public static final int MAXIMUM_RETAINED_MESSAGES = 25;
+
+ private final ListThis API's behavior was changed in SDK version {@link Build.VERSION_CODES#P}. If your
+ * application's target version is less than {@link Build.VERSION_CODES#P}, setting a
+ * conversation title to a non-null value will make {@link #isGroupConversation()} return
+ * {@code true} and passing {@code null} will make it return {@code false}. This behavior
+ * can be overridden by calling {@link #setGroupConversation(boolean)} regardless of SDK
+ * version. In {@link Build.VERSION_CODES#P} and above, this method does not affect group
+ * conversation settings.
+ *
+ * @param conversationTitle Title displayed for this conversation
+ * @return this object for method chaining
+ */
+ public @NonNull MessagingStyle setConversationTitle(
+ @Nullable CharSequence conversationTitle) {
+ mConversationTitle = conversationTitle;
+ return this;
+ }
+
+ /**
+ * Return the title to be displayed on this conversation. Can be {@code null}.
+ */
+ @Nullable
+ public CharSequence getConversationTitle() {
+ return mConversationTitle;
+ }
+
+ /**
+ * Adds a message for display by this notification. Convenience call for a simple
+ * {@link Message} in {@link #addMessage(Message)}
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived in ms since Unix epoch
+ * @param sender A {@link CharSequence} to be used for displaying the name of the
+ * sender. Should be null for messages by the current user, in which case
+ * the platform will insert {@link #getUserDisplayName()}.
+ * Should be unique amongst all individuals in the conversation, and should be
+ * consistent during re-posts of the notification.
+ *
+ * @see Message#Message(CharSequence, long, CharSequence)
+ *
+ * @return this object for method chaining
+ *
+ * @deprecated Use {@link #addMessage(CharSequence, long, Person)} or
+ * {@link #addMessage(Message)}
+ */
+ @Deprecated
+ @NonNull
+ public MessagingStyle addMessage(@Nullable CharSequence text, long timestamp,
+ @Nullable CharSequence sender) {
+ mMessages.add(
+ new Message(text, timestamp, new Person.Builder().setName(sender).build()));
+ if (mMessages.size() > MAXIMUM_RETAINED_MESSAGES) {
+ mMessages.remove(0);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a message for display by this notification. Convenience call for
+ * {@link #addMessage(Message)}.
+ *
+ * @see Message#Message(CharSequence, long, Person)
+ *
+ * @return this for method chaining
+ */
+ public @NonNull MessagingStyle addMessage(@Nullable CharSequence text, long timestamp,
+ @Nullable Person person) {
+ addMessage(new Message(text, timestamp, person));
+ return this;
+ }
+
+ /**
+ * Adds a {@link Message} for display in this notification.
+ *
+ * @param message The {@link Message} to be displayed
+ *
+ * @return this object for method chaining
+ */
+ public @NonNull MessagingStyle addMessage(@Nullable Message message) {
+ if (message != null) {
+ mMessages.add(message);
+ if (mMessages.size() > MAXIMUM_RETAINED_MESSAGES) {
+ mMessages.remove(0);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds a {@link Message} for historic context in this notification.
+ *
+ *
Messages should be added as historic if they are not the main subject of the + * notification but may give context to a conversation. The system may choose to present + * them only when relevant, e.g. when replying to a message through a {@link RemoteInput}. + * + *
The messages should be added in chronologic order, i.e. the oldest first,
+ * the newest last.
+ *
+ * @param message The historic {@link Message} to be added
+ * @return this object for method chaining
+ */
+ public @NonNull MessagingStyle addHistoricMessage(@Nullable Message message) {
+ if (message != null) {
+ mHistoricMessages.add(message);
+ if (mHistoricMessages.size() > MAXIMUM_RETAINED_MESSAGES) {
+ mHistoricMessages.remove(0);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Gets the list of {@code Message} objects that represent the notification.
+ */
+ public @NonNull List Group conversation notifications may display additional group-related context not
+ * present in non-group notifications.
+ *
+ * @param isGroupConversation {@code true} if the conversation represents a group,
+ * {@code false} otherwise.
+ * @return this object for method chaining
+ *
+ * @see #isGroupConversation()
+ */
+ public @NonNull MessagingStyle setGroupConversation(boolean isGroupConversation) {
+ mIsGroupConversation = isGroupConversation;
+ return this;
+ }
+
+ /**
+ * Returns {@code true} if this notification represents a group conversation, otherwise
+ * {@code false}.
+ *
+ * If the application that generated this {@link MessagingStyle} targets an SDK version
+ * less than {@link Build.VERSION_CODES#P} and {@link #setGroupConversation(boolean)}
+ * was not called, this method becomes dependent on whether or not the conversation title is
+ * set; returning {@code true} if the conversation title is a non-null value, or
+ * {@code false} otherwise. This is to maintain backwards compatibility. Regardless, {@link
+ * #setGroupConversation(boolean)} has precedence over this legacy behavior. From
+ * {@link Build.VERSION_CODES#P} forward, {@link #setConversationTitle(CharSequence)} has
+ * no affect on group conversation status.
+ *
+ * @see #setConversationTitle(CharSequence)
+ */
+ public boolean isGroupConversation() {
+ // When target SDK version is < P and the app didn't explicitly set isGroupConversation,
+ // a non-null conversation title dictates if this is a group conversation.
+ if (mBuilder != null
+ && mBuilder.mContext.getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.P
+ && mIsGroupConversation == null) {
+ return mConversationTitle != null;
+ }
+
+ // Default to false if not set.
+ return (mIsGroupConversation != null) ? mIsGroupConversation : false;
+ }
+
+ /**
+ * Retrieves a {@link MessagingStyle} from a {@link Notification}, enabling an application
+ * that has set a {@link MessagingStyle} using {@link NotificationCompat} or
+ * {@link Notification.Builder} to send messaging information to another
+ * application using {@link NotificationCompat}, regardless of the API level of the system.
+ *
+ * @return {@code null} if there is no {@link MessagingStyle} set, or if the SDK version is
+ * < {@code 16} (JellyBean).
+ */
+ @Nullable
+ public static MessagingStyle extractMessagingStyleFromNotification(
+ @NonNull Notification notification) {
+ Style style = Style.extractStyleFromNotification(notification);
+ if (style instanceof MessagingStyle) {
+ return (MessagingStyle) style;
+ }
+ return null;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @NonNull
+ protected String getClassName() {
+ return TEMPLATE_CLASS_NAME;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ // This is called because we need to apply legacy logic before writing MessagingInfo
+ // data into the bundle. This does nothing in >= P, but in < P this will apply the
+ // correct group conversation status to new fields which will then be decoded properly
+ // by #extractMessagingStyleFromNotification.
+ setGroupConversation(isGroupConversation());
+
+ if (Build.VERSION.SDK_INT >= 24) {
+ Object frameworkStyle;
+ if (Build.VERSION.SDK_INT >= 28) {
+ frameworkStyle = Api28Impl.createMessagingStyle(mUser.toAndroidPerson());
+ } else {
+ frameworkStyle =
+ Api24Impl.createMessagingStyle(
+ mUser.getName());
+ }
+
+ for (Message message : mMessages) {
+ Api24Impl.addMessage((Notification.MessagingStyle) frameworkStyle,
+ message.toAndroidMessage());
+ }
+
+ if (Build.VERSION.SDK_INT >= 26) {
+ for (Message historicMessage : mHistoricMessages) {
+ Api26Impl.addHistoricMessage((Notification.MessagingStyle) frameworkStyle,
+ historicMessage.toAndroidMessage());
+ }
+ }
+
+ // In SDK < 28, base Android will assume a MessagingStyle notification is a group
+ // chat if the conversation title is set. In compat, this isn't the case as we've
+ // introduced #setGroupConversation. When we apply these settings to base Android
+ // notifications, we should only set base Android's MessagingStyle conversation
+ // title if it's a group conversation OR SDK >= 28. Otherwise we set the
+ // Notification content title so Android won't think it's a group conversation.
+ if (mIsGroupConversation || Build.VERSION.SDK_INT >= 28) {
+ // If group or non-legacy, set MessagingStyle#mConversationTitle.
+ Api24Impl.setConversationTitle((Notification.MessagingStyle) frameworkStyle,
+ mConversationTitle);
+ }
+
+ // For SDK >= 28, we can simply denote the group conversation status regardless of
+ // if we set the conversation title or not.
+ if (Build.VERSION.SDK_INT >= 28) {
+ Api28Impl.setGroupConversation((Notification.MessagingStyle) frameworkStyle,
+ mIsGroupConversation);
+ }
+ Notification.Builder builder1 = builder.getBuilder();
+ ((Notification.Style) frameworkStyle).setBuilder(builder1);
+ } else {
+ Message latestIncomingMessage = findLatestIncomingMessage();
+ // Set the title
+ if (mConversationTitle != null && mIsGroupConversation) {
+ builder.getBuilder().setContentTitle(mConversationTitle);
+ } else if (latestIncomingMessage != null) {
+ builder.getBuilder().setContentTitle("");
+ if (latestIncomingMessage.getPerson() != null) {
+ builder.getBuilder().setContentTitle(
+ latestIncomingMessage.getPerson().getName());
+ }
+ }
+ // Set the text
+ if (latestIncomingMessage != null) {
+ builder.getBuilder().setContentText(mConversationTitle != null
+ ? makeMessageLine(latestIncomingMessage)
+ : latestIncomingMessage.getText());
+ }
+ // Build a fallback BigTextStyle for API 16-23 devices
+ SpannableStringBuilder completeMessage = new SpannableStringBuilder();
+ boolean showNames = mConversationTitle != null
+ || hasMessagesWithoutSender();
+ for (int i = mMessages.size() - 1; i >= 0; i--) {
+ Message message = mMessages.get(i);
+ CharSequence line;
+ line = showNames ? makeMessageLine(message) : message.getText();
+ if (i != mMessages.size() - 1) {
+ completeMessage.insert(0, "\n");
+ }
+ completeMessage.insert(0, line);
+ }
+ Notification.Builder builder1 = builder.getBuilder();
+ Notification.BigTextStyle style =
+ new Notification.BigTextStyle(builder1);
+ style = style.setBigContentTitle(null);
+ style.bigText(completeMessage);
+ }
+ }
+
+ @Nullable
+ private Message findLatestIncomingMessage() {
+ for (int i = mMessages.size() - 1; i >= 0; i--) {
+ Message message = mMessages.get(i);
+ // Incoming messages have a non-empty sender.
+ if (message.getPerson() != null
+ && !TextUtils.isEmpty(message.getPerson().getName())) {
+ return message;
+ }
+ }
+ if (!mMessages.isEmpty()) {
+ // No incoming messages, fall back to outgoing message
+ return mMessages.get(mMessages.size() - 1);
+ }
+ return null;
+ }
+
+ private boolean hasMessagesWithoutSender() {
+ for (int i = mMessages.size() - 1; i >= 0; i--) {
+ Message message = mMessages.get(i);
+ if (message.getPerson() != null && message.getPerson().getName() == null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private CharSequence makeMessageLine(@NonNull Message message) {
+ BidiFormatter bidi = BidiFormatter.getInstance();
+ SpannableStringBuilder sb = new SpannableStringBuilder();
+ final boolean afterLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+ int color = afterLollipop ? Color.BLACK : Color.WHITE;
+ CharSequence replyName =
+ message.getPerson() == null ? "" : message.getPerson().getName();
+ if (TextUtils.isEmpty(replyName)) {
+ replyName = mUser.getName();
+ color = afterLollipop && mBuilder.getColor() != NotificationCompat.COLOR_DEFAULT
+ ? mBuilder.getColor()
+ : color;
+ }
+ CharSequence senderText = bidi.unicodeWrap(replyName);
+ sb.append(senderText);
+ sb.setSpan(makeFontColorSpan(color),
+ sb.length() - senderText.length(),
+ sb.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE /* flags */);
+ CharSequence text = message.getText() == null ? "" : message.getText();
+ sb.append(" ").append(bidi.unicodeWrap(text));
+ return sb;
+ }
+
+ @NonNull
+ private TextAppearanceSpan makeFontColorSpan(int color) {
+ return new TextAppearanceSpan(null, 0, 0, ColorStateList.valueOf(color), null);
+ }
+
+ @Override
+ public void addCompatExtras(@NonNull Bundle extras) {
+ super.addCompatExtras(extras);
+ extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUser.getName());
+ extras.putBundle(EXTRA_MESSAGING_STYLE_USER, mUser.toBundle());
+
+ extras.putCharSequence(EXTRA_HIDDEN_CONVERSATION_TITLE, mConversationTitle);
+ if (mConversationTitle != null && mIsGroupConversation) {
+ extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle);
+ }
+ if (!mMessages.isEmpty()) {
+ extras.putParcelableArray(EXTRA_MESSAGES,
+ Message.getBundleArrayForMessages(mMessages));
+ }
+ if (!mHistoricMessages.isEmpty()) {
+ extras.putParcelableArray(EXTRA_HISTORIC_MESSAGES,
+ Message.getBundleArrayForMessages(mHistoricMessages));
+ }
+ if (mIsGroupConversation != null) {
+ extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation);
+ }
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @SuppressWarnings("deprecation")
+ protected void restoreFromCompatExtras(@NonNull Bundle extras) {
+ super.restoreFromCompatExtras(extras);
+ mMessages.clear();
+ // Call to #restore requires that there either be a display name OR a user.
+ if (extras.containsKey(EXTRA_MESSAGING_STYLE_USER)) {
+ // New path simply unpacks Person, but checks if there's a valid name.
+ mUser = Person.fromBundle(extras.getBundle(EXTRA_MESSAGING_STYLE_USER));
+ } else {
+ // Legacy extra simply builds Person with a name.
+ mUser = new Person.Builder()
+ .setName(extras.getString(EXTRA_SELF_DISPLAY_NAME))
+ .build();
+ }
+
+ mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
+ if (mConversationTitle == null) {
+ mConversationTitle = extras.getCharSequence(EXTRA_HIDDEN_CONVERSATION_TITLE);
+ }
+ Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
+ if (messages != null) {
+ mMessages.addAll(Message.getMessagesFromBundleArray(messages));
+ }
+ Parcelable[] historicMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES);
+ if (historicMessages != null) {
+ mHistoricMessages.addAll(Message.getMessagesFromBundleArray(historicMessages));
+ }
+ if (extras.containsKey(EXTRA_IS_GROUP_CONVERSATION)) {
+ mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION);
+ }
+ }
+
+ /**
+ */
+ @Override
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ protected void clearCompatExtraKeys(@NonNull Bundle extras) {
+ super.clearCompatExtraKeys(extras);
+ extras.remove(EXTRA_MESSAGING_STYLE_USER);
+ extras.remove(EXTRA_SELF_DISPLAY_NAME);
+ extras.remove(EXTRA_CONVERSATION_TITLE);
+ extras.remove(EXTRA_HIDDEN_CONVERSATION_TITLE);
+ extras.remove(EXTRA_MESSAGES);
+ extras.remove(EXTRA_HISTORIC_MESSAGES);
+ extras.remove(EXTRA_IS_GROUP_CONVERSATION);
+ }
+
+ public static final class Message {
+ static final String KEY_TEXT = "text";
+ static final String KEY_TIMESTAMP = "time";
+ static final String KEY_SENDER = "sender";
+ static final String KEY_DATA_MIME_TYPE = "type";
+ static final String KEY_DATA_URI= "uri";
+ static final String KEY_EXTRAS_BUNDLE = "extras";
+ static final String KEY_PERSON = "person";
+ static final String KEY_NOTIFICATION_PERSON = "sender_person";
+
+ private final CharSequence mText;
+ private final long mTimestamp;
+ @Nullable private final Person mPerson;
+
+ private Bundle mExtras = new Bundle();
+ @Nullable private String mDataMimeType;
+ @Nullable private Uri mDataUri;
+
+ /**
+ * Creates a new {@link Message} with the given text, timestamp, and sender.
+ *
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived in ms since Unix epoch
+ * @param person A {@link Person} whose {@link Person#getName()} value is used as the
+ * display name for the sender. This should be {@code null} for messages by the current
+ * user, in which case, the platform will insert
+ * {@link MessagingStyle#getUserDisplayName()}. A {@link Person}'s key should be
+ * consistent during re-posts of the notification.
+ */
+ public Message(@Nullable CharSequence text, long timestamp, @Nullable Person person) {
+ mText = text;
+ mTimestamp = timestamp;
+ mPerson = person;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived in ms since Unix epoch
+ * @param sender A {@link CharSequence} to be used for displaying the name of the
+ * sender. Should be
+ * Notification Listeners including the System UI need permission to access the
+ * data the Uri points to. The recommended ways to do this are:
+ * Instead of providing a notification that is completely custom, a developer can set this
+ * style and still obtain system decorations like the notification header with the expand
+ * affordance and actions.
+ *
+ * Use {@link Builder#setCustomContentView(RemoteViews)},
+ * {@link Builder#setCustomBigContentView(RemoteViews)} and
+ * {@link Builder#setCustomHeadsUpContentView(RemoteViews)} to set the
+ * corresponding custom views to display.
+ *
+ * To use this style with your Notification, feed it to
+ * {@link Builder#setStyle(Style)} like so:
+ * If you are using this style, consider using the corresponding styles like
+ * {@link R.style#TextAppearance_Compat_Notification} or
+ * {@link R.style#TextAppearance_Compat_Notification_Title} in
+ * your custom views in order to get the correct styling on each platform version.
+ */
+ public static class DecoratedCustomViewStyle extends Style {
+
+ private static final String TEMPLATE_CLASS_NAME =
+ "androidx.core.app.NotificationCompat$DecoratedCustomViewStyle";
+
+ private static final int MAX_ACTION_BUTTONS = 3;
+
+ public DecoratedCustomViewStyle() {
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @NonNull
+ protected String getClassName() {
+ return TEMPLATE_CLASS_NAME;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public boolean displayCustomViewInline() {
+ return true;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Notification.Builder builder1 = builder.getBuilder();
+ builder1.setStyle(Api24Impl.createDecoratedCustomViewStyle());
+
+ }
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom content view required
+ return null;
+ }
+ if (mBuilder.getContentView() == null) {
+ // No special content view
+ return null;
+ }
+ return createRemoteViews(mBuilder.getContentView(), false);
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom big content view required
+ return null;
+ }
+ RemoteViews bigContentView = mBuilder.getBigContentView();
+ RemoteViews innerView = bigContentView != null
+ ? bigContentView
+ : mBuilder.getContentView();
+ if (innerView == null) {
+ // No expandable notification
+ return null;
+ }
+ return createRemoteViews(innerView, true);
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public RemoteViews makeHeadsUpContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom heads up content view required
+ return null;
+ }
+ RemoteViews headsUp = mBuilder.getHeadsUpContentView();
+ RemoteViews innerView = headsUp != null ? headsUp : mBuilder.getContentView();
+ if (headsUp == null) {
+ // No expandable notification
+ return null;
+ }
+ return createRemoteViews(innerView, true);
+ }
+
+ private RemoteViews createRemoteViews(RemoteViews innerView, boolean showActions) {
+ RemoteViews remoteViews = applyStandardTemplate(true /* showSmallIcon */,
+ R.layout.notification_template_custom_big, false /* fitIn1U */);
+ remoteViews.removeAllViews(R.id.actions);
+ boolean actionsVisible = false;
+
+ // In the UI contextual actions appear separately from the standard actions, so we
+ // filter them out here.
+ List
+ * As of Android {@link android.os.Build.VERSION_CODES#N},
+ * action button icons will not be displayed on action buttons, but are still required and
+ * are available to
+ * {@link android.service.notification.NotificationListenerService notification listeners},
+ * which may display them in other contexts, for example on a wearable device.
+ *
+ * Apps should use {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)}
+ * or {@link NotificationCompat.Builder#addAction(NotificationCompat.Action)}
+ * to attach actions.
+ */
+ public static class Action {
+ /**
+ * {@link SemanticAction}: No semantic action defined.
+ */
+ public static final int SEMANTIC_ACTION_NONE = 0;
+
+ /**
+ * {@link SemanticAction}: Reply to a conversation, chat, group, or wherever replies
+ * may be appropriate.
+ */
+ public static final int SEMANTIC_ACTION_REPLY = 1;
+
+ /**
+ * {@link SemanticAction}: Mark content as read.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_READ = 2;
+
+ /**
+ * {@link SemanticAction}: Mark content as unread.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_UNREAD = 3;
+
+ /**
+ * {@link SemanticAction}: Delete the content associated with the notification. This
+ * could mean deleting an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_DELETE = 4;
+
+ /**
+ * {@link SemanticAction}: Archive the content associated with the notification. This
+ * could mean archiving an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_ARCHIVE = 5;
+
+ /**
+ * {@link SemanticAction}: Mute the content associated with the notification. This could
+ * mean silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_MUTE = 6;
+
+ /**
+ * {@link SemanticAction}: Unmute the content associated with the notification. This could
+ * mean un-silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_UNMUTE = 7;
+
+ /**
+ * {@link SemanticAction}: Mark content with a thumbs up.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_UP = 8;
+
+ /**
+ * {@link SemanticAction}: Mark content with a thumbs down.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9;
+
+ /**
+ * {@link SemanticAction}: Call a contact, group, etc.
+ */
+ public static final int SEMANTIC_ACTION_CALL = 10;
+
+ static final String EXTRA_SHOWS_USER_INTERFACE =
+ "android.support.action.showsUserInterface";
+
+ static final String EXTRA_SEMANTIC_ACTION = "android.support.action.semanticAction";
+
+ final Bundle mExtras;
+ @Nullable private IconCompat mIcon;
+ private final RemoteInput[] mRemoteInputs;
+
+ /**
+ * Holds {@link RemoteInput}s that only accept data, meaning
+ * {@link RemoteInput#getAllowFreeFormInput} is false, {@link RemoteInput#getChoices}
+ * is null or empty, and {@link RemoteInput#getAllowedDataTypes is non-null and not
+ * empty. These {@link RemoteInput}s will be ignored by devices that do not
+ * support non-text-based {@link RemoteInput}s. See {@link Builder#build}.
+ *
+ * You can test if a RemoteInput matches these constraints using
+ * {@link RemoteInput#isDataOnly}.
+ */
+ private final RemoteInput[] mDataOnlyRemoteInputs;
+
+ private boolean mAllowGeneratedReplies;
+ boolean mShowsUserInterface = true;
+
+ private final @SemanticAction int mSemanticAction;
+ private final boolean mIsContextual;
+
+ /**
+ * Small icon representing the action.
+ *
+ * @deprecated Use {@link #getIconCompat()} instead.
+ */
+ @Deprecated
+ public int icon;
+ /**
+ * Title of the action.
+ */
+ public CharSequence title;
+ /**
+ * Intent to send when the user invokes this action. May be null, in which case the action
+ * may be rendered in a disabled presentation.
+ */
+ public @Nullable PendingIntent actionIntent;
+
+ private boolean mAuthenticationRequired;
+
+ public Action(int icon, @Nullable CharSequence title, @Nullable PendingIntent intent) {
+ this(icon == 0 ? null : IconCompat.createWithResource(null, "", icon), title, intent);
+ }
+
+ /**
+ * Note: For devices running an Android version strictly lower than API
+ * level 23 this constructor only supports resource-ID based IconCompat objects.
+ */
+ public Action(@Nullable IconCompat icon, @Nullable CharSequence title,
+ @Nullable PendingIntent intent) {
+ this(icon, title, intent, new Bundle(), null, null, true, SEMANTIC_ACTION_NONE, true,
+ false /* isContextual */, false /* authRequired */);
+ }
+
+ Action(int icon, @Nullable CharSequence title, @Nullable PendingIntent intent,
+ @Nullable Bundle extras,
+ @Nullable RemoteInput[] remoteInputs, @Nullable RemoteInput[] dataOnlyRemoteInputs,
+ boolean allowGeneratedReplies, @SemanticAction int semanticAction,
+ boolean showsUserInterface, boolean isContextual, boolean requireAuth) {
+ this(icon == 0 ? null : IconCompat.createWithResource(null, "", icon), title,
+ intent, extras, remoteInputs, dataOnlyRemoteInputs, allowGeneratedReplies,
+ semanticAction, showsUserInterface, isContextual, requireAuth);
+ }
+
+ // Package private access to avoid adding a SyntheticAccessor for the Action.Builder class.
+ @SuppressWarnings("deprecation")
+ Action(@Nullable IconCompat icon, @Nullable CharSequence title,
+ @Nullable PendingIntent intent, @Nullable Bundle extras,
+ @Nullable RemoteInput[] remoteInputs, @Nullable RemoteInput[] dataOnlyRemoteInputs,
+ boolean allowGeneratedReplies, @SemanticAction int semanticAction,
+ boolean showsUserInterface, boolean isContextual, boolean requireAuth) {
+ this.mIcon = icon;
+ if (icon != null && icon.getType() == IconCompat.TYPE_RESOURCE) {
+ this.icon = icon.getResId();
+ }
+ this.title = NotificationCompat.Builder.limitCharSequenceLength(title);
+ this.actionIntent = intent;
+ this.mExtras = extras != null ? extras : new Bundle();
+ this.mRemoteInputs = remoteInputs;
+ this.mDataOnlyRemoteInputs = dataOnlyRemoteInputs;
+ this.mAllowGeneratedReplies = allowGeneratedReplies;
+ this.mSemanticAction = semanticAction;
+ this.mShowsUserInterface = showsUserInterface;
+ this.mIsContextual = isContextual;
+ this.mAuthenticationRequired = requireAuth;
+ }
+
+ /**
+ * @deprecated use {@link #getIconCompat()} instead.
+ */
+ @SuppressWarnings("deprecation")
+ @Deprecated
+ public int getIcon() {
+ return icon;
+ }
+
+ /**
+ * Return the icon associated with this Action.
+ */
+ @SuppressWarnings("deprecation")
+ public @Nullable IconCompat getIconCompat() {
+ if (mIcon == null && icon != 0) {
+ mIcon = IconCompat.createWithResource(null, "", icon);
+ }
+ return mIcon;
+ }
+
+ public @Nullable CharSequence getTitle() {
+ return title;
+ }
+
+ public @Nullable PendingIntent getActionIntent() {
+ return actionIntent;
+ }
+
+ /**
+ * Get additional metadata carried around with this Action.
+ */
+ public @NonNull Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Return whether the platform should automatically generate possible replies for this
+ * {@link Action}
+ */
+ public boolean getAllowGeneratedReplies() {
+ return mAllowGeneratedReplies;
+ }
+
+ /**
+ * Returns whether the OS should only send this action's {@link PendingIntent} on an
+ * unlocked device.
+ *
+ * If the device is locked when the action is invoked, the OS should show the keyguard and
+ * require successful authentication before invoking the intent.
+ */
+ public boolean isAuthenticationRequired() {
+ return mAuthenticationRequired;
+ }
+
+ /**
+ * Get the list of inputs to be collected from the user when this action is sent.
+ * May return null if no remote inputs were added. Only returns inputs which accept
+ * a text input. For inputs which only accept data use {@link #getDataOnlyRemoteInputs}.
+ */
+ public @Nullable RemoteInput[] getRemoteInputs() {
+ return mRemoteInputs;
+ }
+
+ /**
+ * Returns the {@link SemanticAction} associated with this {@link Action}. A
+ * {@link SemanticAction} denotes what an {@link Action}'s {@link PendingIntent} will do
+ * (eg. reply, mark as read, delete, etc).
+ *
+ * @see SemanticAction
+ */
+ public @SemanticAction int getSemanticAction() {
+ return mSemanticAction;
+ }
+
+ /**
+ * Returns whether this is a contextual Action, i.e. whether the action is dependent on the
+ * notification message body. An example of a contextual action could be an action opening a
+ * map application with an address shown in the notification.
+ */
+ public boolean isContextual() {
+ return mIsContextual;
+ }
+
+ /**
+ * Get the list of inputs to be collected from the user that ONLY accept data when this
+ * action is sent. These remote inputs are guaranteed to return true on a call to
+ * {@link RemoteInput#isDataOnly}.
+ *
+ * May return null if no data-only remote inputs were added.
+ *
+ * This method exists so that legacy RemoteInput collectors that pre-date the addition
+ * of non-textual RemoteInputs do not access these remote inputs.
+ */
+ public @Nullable RemoteInput[] getDataOnlyRemoteInputs() {
+ return mDataOnlyRemoteInputs;
+ }
+
+ /**
+ * Return whether or not triggering this {@link Action}'s {@link PendingIntent} will open a
+ * user interface.
+ */
+ public boolean getShowsUserInterface() {
+ return mShowsUserInterface;
+ }
+
+ /**
+ * Builder class for {@link Action} objects.
+ */
+ public static final class Builder {
+ private final IconCompat mIcon;
+ private final CharSequence mTitle;
+ private final PendingIntent mIntent;
+ private boolean mAllowGeneratedReplies = true;
+ private final Bundle mExtras;
+ private ArrayList Note: For devices running an Android version strictly lower than
+ * API level 23 this constructor only supports resource-ID based IconCompat objects.
+ * @param icon icon to show for this action
+ * @param title the title of the action
+ * @param intent the {@link PendingIntent} to fire when users trigger this action
+ */
+ public Builder(@Nullable IconCompat icon, @Nullable CharSequence title,
+ @Nullable PendingIntent intent) {
+ this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE, true,
+ false /* isContextual */, false /* authRequired */);
+ }
+
+ /**
+ * Construct a new builder for {@link Action} object.
+ * @param icon icon to show for this action
+ * @param title the title of the action
+ * @param intent the {@link PendingIntent} to fire when users trigger this action
+ */
+ public Builder(int icon, @Nullable CharSequence title, @Nullable PendingIntent intent) {
+ this(icon == 0 ? null : IconCompat.createWithResource(null, "", icon), title,
+ intent,
+ new Bundle(),
+ null,
+ true,
+ SEMANTIC_ACTION_NONE,
+ true,
+ false /* isContextual */, false /* authRequired */);
+ }
+
+ /**
+ * Construct a new builder for {@link Action} object using the fields from an
+ * {@link Action}.
+ * @param action the action to read fields from.
+ */
+ public Builder(@NonNull Action action) {
+ this(action.getIconCompat(), action.title, action.actionIntent,
+ new Bundle(action.mExtras),
+ action.getRemoteInputs(), action.getAllowGeneratedReplies(),
+ action.getSemanticAction(), action.mShowsUserInterface,
+ action.isContextual(), action.isAuthenticationRequired());
+ }
+
+ private Builder(@Nullable IconCompat icon, @Nullable CharSequence title,
+ @Nullable PendingIntent intent, @NonNull Bundle extras,
+ @Nullable RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction, boolean showsUserInterface,
+ boolean isContextual, boolean authRequired) {
+ mIcon = icon;
+ mTitle = NotificationCompat.Builder.limitCharSequenceLength(title);
+ mIntent = intent;
+ mExtras = extras;
+ mRemoteInputs = remoteInputs == null ? null : new ArrayList<>(
+ Arrays.asList(remoteInputs));
+ mAllowGeneratedReplies = allowGeneratedReplies;
+ mSemanticAction = semanticAction;
+ mShowsUserInterface = showsUserInterface;
+ mIsContextual = isContextual;
+ mAuthenticationRequired = authRequired;
+ }
+
+ /**
+ * Merge additional metadata into this builder.
+ *
+ * Values within the Bundle will replace existing extras values in this Builder.
+ *
+ * @see NotificationCompat.Action#getExtras
+ */
+ public @NonNull Builder addExtras(@Nullable Bundle extras) {
+ if (extras != null) {
+ mExtras.putAll(extras);
+ }
+ return this;
+ }
+
+ /**
+ * Get the metadata Bundle used by this Builder.
+ *
+ * The returned Bundle is shared with this Builder.
+ */
+ public @NonNull Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Add an input to be collected from the user when this action is sent.
+ * Response values can be retrieved from the fired intent by using the
+ * {@link RemoteInput#getResultsFromIntent} function.
+ * @param remoteInput a {@link RemoteInput} to add to the action
+ * @return this object for method chaining
+ */
+ public @NonNull Builder addRemoteInput(@Nullable RemoteInput remoteInput) {
+ if (mRemoteInputs == null) {
+ mRemoteInputs = new ArrayList<>();
+ }
+ if (remoteInput != null) {
+ mRemoteInputs.add(remoteInput);
+ }
+ return this;
+ }
+
+ /**
+ * Set whether the platform should automatically generate possible replies to add to
+ * {@link RemoteInput#getChoices()}. If the {@link Action} doesn't have a
+ * {@link RemoteInput}, this has no effect.
+ * @param allowGeneratedReplies {@code true} to allow generated replies, {@code false}
+ * otherwise
+ * @return this object for method chaining
+ * The default value is {@code true}
+ */
+ public @NonNull Builder setAllowGeneratedReplies(boolean allowGeneratedReplies) {
+ mAllowGeneratedReplies = allowGeneratedReplies;
+ return this;
+ }
+
+ /**
+ * Sets the {@link SemanticAction} for this {@link Action}. A {@link SemanticAction}
+ * denotes what an {@link Action}'s {@link PendingIntent} will do (eg. reply, mark
+ * as read, delete, etc).
+ * @param semanticAction a {@link SemanticAction} defined within {@link Action} with
+ * {@code SEMANTIC_ACTION_} prefixes
+ * @return this object for method chaining
+ */
+ public @NonNull Builder setSemanticAction(@SemanticAction int semanticAction) {
+ mSemanticAction = semanticAction;
+ return this;
+ }
+
+ /**
+ * Sets whether this {@link Action} is a contextual action, i.e. whether the action is
+ * dependent on the notification message body. An example of a contextual action could
+ * be an action opening a map application with an address shown in the notification.
+ */
+ public @NonNull Builder setContextual(boolean isContextual) {
+ mIsContextual = isContextual;
+ return this;
+ }
+
+ /**
+ * From API 31, sets whether the OS should only send this action's {@link PendingIntent}
+ * on an unlocked device.
+ *
+ * If this is true and the device is locked when the action is invoked, the OS will
+ * show the keyguard and require successful authentication before invoking the intent.
+ * If this is false and the device is locked, the OS will decide whether authentication
+ * should be required.
+ */
+ @NonNull
+ public Builder setAuthenticationRequired(boolean authenticationRequired) {
+ mAuthenticationRequired = authenticationRequired;
+ return this;
+ }
+
+ /**
+ * Set whether or not this {@link Action}'s {@link PendingIntent} will open a user
+ * interface.
+ * @param showsUserInterface {@code true} if this {@link Action}'s {@link PendingIntent}
+ * will open a user interface, otherwise {@code false}
+ * @return this object for method chaining
+ * The default value is {@code true}
+ */
+ public @NonNull Builder setShowsUserInterface(boolean showsUserInterface) {
+ mShowsUserInterface = showsUserInterface;
+ return this;
+ }
+
+ /**
+ * Apply an extender to this action builder. Extenders may be used to add
+ * metadata or change options on this builder.
+ */
+ public @NonNull Builder extend(@NonNull Extender extender) {
+ extender.extend(this);
+ return this;
+ }
+
+ /**
+ * Throws an NPE if we are building a contextual action missing one of the fields
+ * necessary to display the action.
+ */
+ private void checkContextualActionNullFields() {
+ if (!mIsContextual) return;
+
+ if (mIntent == null) {
+ throw new NullPointerException(
+ "Contextual Actions must contain a valid PendingIntent");
+ }
+ }
+
+ /**
+ * Combine all of the options that have been set and return a new {@link Action}
+ * object.
+ * @return the built action
+ * @throws NullPointerException if this is a contextual Action and its Intent is
+ * null.
+ */
+ public @NonNull Action build() {
+ checkContextualActionNullFields();
+
+ List See
+ * Creating Notifications
+ * for Android Wear for more information on how to use this class.
+ *
+ * To create a notification with wearable extensions:
+ * Wearable extensions can be accessed on an existing notification by using the
+ * {@code WearableExtender(Notification)} constructor,
+ * and then using the {@code get} methods to access values.
+ *
+ * For custom display notifications created using {@link #setDisplayIntent},
+ * the default is {@link #SIZE_MEDIUM}. All other notifications size automatically based
+ * on their content.
+ *
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public static final int SIZE_DEFAULT = 0;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with an extra small size.
+ * This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ *
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public static final int SIZE_XSMALL = 1;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with a small size.
+ * This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ *
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public static final int SIZE_SMALL = 2;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with a medium size.
+ * This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ *
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public static final int SIZE_MEDIUM = 3;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with a large size.
+ * This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ *
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public static final int SIZE_LARGE = 4;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * full screen.
+ * This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ *
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public static final int SIZE_FULL_SCREEN = 5;
+
+ /**
+ * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on for a
+ * short amount of time when this notification is displayed on the screen. This
+ * is the default value.
+ *
+ * @deprecated This feature is no longer supported.
+ */
+ @Deprecated
+ public static final int SCREEN_TIMEOUT_SHORT = 0;
+
+ /**
+ * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on
+ * for a longer amount of time when this notification is displayed on the screen.
+ * @deprecated This feature is no longer supported.
+ */
+ @Deprecated
+ public static final int SCREEN_TIMEOUT_LONG = -1;
+
+ /** Notification extra which contains wearable extensions */
+ private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS";
+
+ // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options.
+ private static final String KEY_ACTIONS = "actions";
+ private static final String KEY_FLAGS = "flags";
+ private static final String KEY_DISPLAY_INTENT = "displayIntent";
+ private static final String KEY_PAGES = "pages";
+ private static final String KEY_BACKGROUND = "background";
+ private static final String KEY_CONTENT_ICON = "contentIcon";
+ private static final String KEY_CONTENT_ICON_GRAVITY = "contentIconGravity";
+ private static final String KEY_CONTENT_ACTION_INDEX = "contentActionIndex";
+ private static final String KEY_CUSTOM_SIZE_PRESET = "customSizePreset";
+ private static final String KEY_CUSTOM_CONTENT_HEIGHT = "customContentHeight";
+ private static final String KEY_GRAVITY = "gravity";
+ private static final String KEY_HINT_SCREEN_TIMEOUT = "hintScreenTimeout";
+ private static final String KEY_DISMISSAL_ID = "dismissalId";
+ private static final String KEY_BRIDGE_TAG = "bridgeTag";
+
+ // Flags bitwise-ored to mFlags
+ private static final int FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE = 0x1;
+ private static final int FLAG_HINT_HIDE_ICON = 1 << 1;
+ private static final int FLAG_HINT_SHOW_BACKGROUND_ONLY = 1 << 2;
+ private static final int FLAG_START_SCROLL_BOTTOM = 1 << 3;
+ private static final int FLAG_HINT_AVOID_BACKGROUND_CLIPPING = 1 << 4;
+ private static final int FLAG_BIG_PICTURE_AMBIENT = 1 << 5;
+ private static final int FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY = 1 << 6;
+
+ // Default value for flags integer
+ private static final int DEFAULT_FLAGS = FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE;
+
+ private static final int DEFAULT_CONTENT_ICON_GRAVITY = GravityCompat.END;
+ private static final int DEFAULT_GRAVITY = Gravity.BOTTOM;
+
+ private ArrayList When wearable actions are added using this method, the set of actions that
+ * show on a wearable device splits from devices that only show actions added
+ * using {@link NotificationCompat.Builder#addAction}. This allows for customization
+ * of which actions display on different devices.
+ *
+ * @param action the action to add to this notification
+ * @return this object for method chaining
+ * @see NotificationCompat.Action
+ */
+ public @NonNull WearableExtender addAction(@NonNull Action action) {
+ mActions.add(action);
+ return this;
+ }
+
+ /**
+ * Adds wearable actions to this notification.
+ *
+ * When wearable actions are added using this method, the set of actions that
+ * show on a wearable device splits from devices that only show actions added
+ * using {@link NotificationCompat.Builder#addAction}. This allows for customization
+ * of which actions display on different devices.
+ *
+ * @param actions the actions to add to this notification
+ * @return this object for method chaining
+ * @see NotificationCompat.Action
+ */
+ public @NonNull WearableExtender addActions(@NonNull List The activity to launch needs to allow embedding, must be exported, and
+ * should have an empty task affinity. It is also recommended to use the device
+ * default light theme.
+ *
+ * Example AndroidManifest.xml entry:
+ * If wearable specific actions were added to the main notification, this index will
+ * apply to that list, otherwise it will apply to the regular actions list.
+ *
+ * @return the action index or {@link #UNSET_ACTION_INDEX} if no action was selected.
+ */
+ public int getContentAction() {
+ return mContentActionIndex;
+ }
+
+ /**
+ * Set the gravity that this notification should have within the available viewport space.
+ * Supported values include {@link android.view.Gravity#TOP},
+ * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}.
+ * The default value is {@link android.view.Gravity#BOTTOM}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setGravity(int gravity) {
+ mGravity = gravity;
+ return this;
+ }
+
+ /**
+ * Get the gravity that this notification should have within the available viewport space.
+ * Supported values include {@link android.view.Gravity#TOP},
+ * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}.
+ * The default value is {@link android.view.Gravity#BOTTOM}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * Set the custom size preset for the display of this notification out of the available
+ * presets found in {@link NotificationCompat.WearableExtender}, e.g.
+ * {@link #SIZE_LARGE}.
+ * Some custom size presets are only applicable for custom display notifications created
+ * using {@link NotificationCompat.WearableExtender#setDisplayIntent}. Check the
+ * documentation for the preset in question. See also
+ * {@link #setCustomContentHeight} and {@link #getCustomSizePreset}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setCustomSizePreset(int sizePreset) {
+ mCustomSizePreset = sizePreset;
+ return this;
+ }
+
+ /**
+ * Get the custom size preset for the display of this notification out of the available
+ * presets found in {@link NotificationCompat.WearableExtender}, e.g.
+ * {@link #SIZE_LARGE}.
+ * Some custom size presets are only applicable for custom display notifications created
+ * using {@link #setDisplayIntent}. Check the documentation for the preset in question.
+ * See also {@link #setCustomContentHeight} and {@link #setCustomSizePreset}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public int getCustomSizePreset() {
+ return mCustomSizePreset;
+ }
+
+ /**
+ * Set the custom height in pixels for the display of this notification's content.
+ * This option is only available for custom display notifications created
+ * using {@link NotificationCompat.WearableExtender#setDisplayIntent}. See also
+ * {@link NotificationCompat.WearableExtender#setCustomSizePreset} and
+ * {@link #getCustomContentHeight}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setCustomContentHeight(int height) {
+ mCustomContentHeight = height;
+ return this;
+ }
+
+ /**
+ * Get the custom height in pixels for the display of this notification's content.
+ * This option is only available for custom display notifications created
+ * using {@link #setDisplayIntent}. See also {@link #setCustomSizePreset} and
+ * {@link #setCustomContentHeight}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public int getCustomContentHeight() {
+ return mCustomContentHeight;
+ }
+
+ /**
+ * Set whether the scrolling position for the contents of this notification should start
+ * at the bottom of the contents instead of the top when the contents are too long to
+ * display within the screen. Default is false (start scroll at the top).
+ */
+ public @NonNull WearableExtender setStartScrollBottom(boolean startScrollBottom) {
+ setFlag(FLAG_START_SCROLL_BOTTOM, startScrollBottom);
+ return this;
+ }
+
+ /**
+ * Get whether the scrolling position for the contents of this notification should start
+ * at the bottom of the contents instead of the top when the contents are too long to
+ * display within the screen. Default is false (start scroll at the top).
+ */
+ public boolean getStartScrollBottom() {
+ return (mFlags & FLAG_START_SCROLL_BOTTOM) != 0;
+ }
+
+ /**
+ * Set whether the content intent is available when the wearable device is not connected
+ * to a companion device. The user can still trigger this intent when the wearable device
+ * is offline, but a visual hint will indicate that the content intent may not be available.
+ * Defaults to true.
+ */
+ public @NonNull WearableExtender setContentIntentAvailableOffline(
+ boolean contentIntentAvailableOffline) {
+ setFlag(FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE, contentIntentAvailableOffline);
+ return this;
+ }
+
+ /**
+ * Get whether the content intent is available when the wearable device is not connected
+ * to a companion device. The user can still trigger this intent when the wearable device
+ * is offline, but a visual hint will indicate that the content intent may not be available.
+ * Defaults to true.
+ */
+ public boolean getContentIntentAvailableOffline() {
+ return (mFlags & FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE) != 0;
+ }
+
+ /**
+ * Set a hint that this notification's icon should not be displayed.
+ * @param hintHideIcon {@code true} to hide the icon, {@code false} otherwise.
+ * @return this object for method chaining
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setHintHideIcon(boolean hintHideIcon) {
+ setFlag(FLAG_HINT_HIDE_ICON, hintHideIcon);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's icon should not be displayed.
+ * @return {@code true} if this icon should not be displayed, false otherwise.
+ * The default value is {@code false} if this was never set.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public boolean getHintHideIcon() {
+ return (mFlags & FLAG_HINT_HIDE_ICON) != 0;
+ }
+
+ /**
+ * Set a visual hint that only the background image of this notification should be
+ * displayed, and other semantic content should be hidden. This hint is only applicable
+ * to sub-pages added using {@link #addPage}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setHintShowBackgroundOnly(boolean hintShowBackgroundOnly) {
+ setFlag(FLAG_HINT_SHOW_BACKGROUND_ONLY, hintShowBackgroundOnly);
+ return this;
+ }
+
+ /**
+ * Get a visual hint that only the background image of this notification should be
+ * displayed, and other semantic content should be hidden. This hint is only applicable
+ * to sub-pages added using {@link NotificationCompat.WearableExtender#addPage}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public boolean getHintShowBackgroundOnly() {
+ return (mFlags & FLAG_HINT_SHOW_BACKGROUND_ONLY) != 0;
+ }
+
+ /**
+ * Set a hint that this notification's background should not be clipped if possible,
+ * and should instead be resized to fully display on the screen, retaining the aspect
+ * ratio of the image. This can be useful for images like barcodes or qr codes.
+ * @param hintAvoidBackgroundClipping {@code true} to avoid clipping if possible.
+ * @return this object for method chaining
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ @NonNull
+ public WearableExtender setHintAvoidBackgroundClipping(
+ boolean hintAvoidBackgroundClipping) {
+ setFlag(FLAG_HINT_AVOID_BACKGROUND_CLIPPING, hintAvoidBackgroundClipping);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's background should not be clipped if possible,
+ * and should instead be resized to fully display on the screen, retaining the aspect
+ * ratio of the image. This can be useful for images like barcodes or qr codes.
+ * @return {@code true} if it's ok if the background is clipped on the screen, false
+ * otherwise. The default value is {@code false} if this was never set.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public boolean getHintAvoidBackgroundClipping() {
+ return (mFlags & FLAG_HINT_AVOID_BACKGROUND_CLIPPING) != 0;
+ }
+
+ /**
+ * Set a hint that the screen should remain on for at least this duration when
+ * this notification is displayed on the screen.
+ * @param timeout The requested screen timeout in milliseconds. Can also be either
+ * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}.
+ * @return this object for method chaining
+ *
+ * @deprecated This method has no effect.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setHintScreenTimeout(int timeout) {
+ mHintScreenTimeout = timeout;
+ return this;
+ }
+
+ /**
+ * Get the duration, in milliseconds, that the screen should remain on for
+ * when this notification is displayed.
+ * @return the duration in milliseconds if > 0, or either one of the sentinel values
+ * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}.
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public int getHintScreenTimeout() {
+ return mHintScreenTimeout;
+ }
+
+ /**
+ * Set a hint that this notification's {@link BigPictureStyle} (if present) should be
+ * converted to low-bit and displayed in ambient mode, especially useful for barcodes and
+ * qr codes, as well as other simple black-and-white tickets.
+ * @param hintAmbientBigPicture {@code true} to enable converstion and ambient.
+ * @return this object for method chaining
+ * @deprecated This feature is no longer supported.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setHintAmbientBigPicture(boolean hintAmbientBigPicture) {
+ setFlag(FLAG_BIG_PICTURE_AMBIENT, hintAmbientBigPicture);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's {@link BigPictureStyle} (if present) should be
+ * converted to low-bit and displayed in ambient mode, especially useful for barcodes and
+ * qr codes, as well as other simple black-and-white tickets.
+ * @return {@code true} if it should be displayed in ambient, false otherwise
+ * otherwise. The default value is {@code false} if this was never set.
+ * @deprecated This feature is no longer supported.
+ */
+ @Deprecated
+ public boolean getHintAmbientBigPicture() {
+ return (mFlags & FLAG_BIG_PICTURE_AMBIENT) != 0;
+ }
+
+ /**
+ * Set a hint that this notification's content intent will launch an {@link Activity}
+ * directly, telling the platform that it can generate the appropriate transitions.
+ * @param hintContentIntentLaunchesActivity {@code true} if the content intent will launch
+ * an activity and transitions should be generated, false otherwise.
+ * @return this object for method chaining
+ */
+ public @NonNull WearableExtender setHintContentIntentLaunchesActivity(
+ boolean hintContentIntentLaunchesActivity) {
+ setFlag(FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY, hintContentIntentLaunchesActivity);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's content intent will launch an {@link Activity}
+ * directly, telling the platform that it can generate the appropriate transitions
+ * @return {@code true} if the content intent will launch an activity and transitions should
+ * be generated, false otherwise. The default value is {@code false} if this was never set.
+ */
+ public boolean getHintContentIntentLaunchesActivity() {
+ return (mFlags & FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY) != 0;
+ }
+
+ /**
+ * Sets the dismissal id for this notification. If a notification is posted with a
+ * dismissal id, then when that notification is canceled, notifications on other wearables
+ * and the paired Android phone having that same dismissal id will also be canceled. See
+ * Adding Wearable Features to
+ * Notifications for more information.
+ * @param dismissalId the dismissal id of the notification.
+ * @return this object for method chaining
+ */
+ public @NonNull WearableExtender setDismissalId(@Nullable String dismissalId) {
+ mDismissalId = dismissalId;
+ return this;
+ }
+
+ /**
+ * Returns the dismissal id of the notification.
+ * @return the dismissal id of the notification or null if it has not been set.
+ */
+ public @Nullable String getDismissalId() {
+ return mDismissalId;
+ }
+
+ /**
+ * Sets a bridge tag for this notification. A bridge tag can be set for notifications
+ * posted from a phone to provide finer-grained control on what notifications are bridged
+ * to wearables. See Adding
+ * Wearable Features to Notifications for more information.
+ * @param bridgeTag the bridge tag of the notification.
+ * @return this object for method chaining
+ */
+ public @NonNull WearableExtender setBridgeTag(@Nullable String bridgeTag) {
+ mBridgeTag = bridgeTag;
+ return this;
+ }
+
+ /**
+ * Returns the bridge tag of the notification.
+ * @return the bridge tag or null if not present.
+ */
+ public @Nullable String getBridgeTag() {
+ return mBridgeTag;
+ }
+
+ private void setFlag(int mask, boolean value) {
+ if (value) {
+ mFlags |= mask;
+ } else {
+ mFlags &= ~mask;
+ }
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification.WearableExtender} methods which
+ * were added in API 20; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(20)
+ static class Api20Impl {
+ private Api20Impl() { }
+
+ @DoNotInline
+ static Notification.Action.Builder createBuilder(int icon, CharSequence title,
+ PendingIntent intent) {
+ return new Notification.Action.Builder(icon, title, intent);
+ }
+
+ @DoNotInline
+ static Notification.Action.Builder addExtras(Notification.Action.Builder builder,
+ Bundle extras) {
+ return builder.addExtras(extras);
+ }
+
+ @DoNotInline
+ static Notification.Action.Builder addRemoteInput(Notification.Action.Builder builder,
+ android.app.RemoteInput remoteInput) {
+ return builder.addRemoteInput(remoteInput);
+ }
+
+ @DoNotInline
+ static Notification.Action build(Notification.Action.Builder builder) {
+ return builder.build();
+ }
+
+ @DoNotInline
+ public static Action getActionCompatFromAction(ArrayList Helper class to add Android Auto extensions to notifications. To create a notification
+ * with car extensions:
+ *
+ * Car extensions can be accessed on an existing notification by using the
+ * {@code CarExtender(Notification)} constructor, and then using the {@code get} methods
+ * to access values.
+ */
+ public static final class CarExtender implements Extender {
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS";
+ private static final String EXTRA_LARGE_ICON = "large_icon";
+ private static final String EXTRA_CONVERSATION = "car_conversation";
+ private static final String EXTRA_COLOR = "app_color";
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ static final String EXTRA_INVISIBLE_ACTIONS = "invisible_actions";
+
+ private static final String KEY_AUTHOR = "author";
+ private static final String KEY_TEXT = "text";
+ private static final String KEY_MESSAGES = "messages";
+ private static final String KEY_REMOTE_INPUT = "remote_input";
+ private static final String KEY_ON_REPLY = "on_reply";
+ private static final String KEY_ON_READ = "on_read";
+ private static final String KEY_PARTICIPANTS = "participants";
+ private static final String KEY_TIMESTAMP = "timestamp";
+
+ private Bitmap mLargeIcon;
+ private UnreadConversation mUnreadConversation;
+ private int mColor = NotificationCompat.COLOR_DEFAULT;
+
+ /**
+ * Create a {@link CarExtender} with default options.
+ */
+ public CarExtender() {
+ }
+
+ /**
+ * Create a {@link CarExtender} from the CarExtender options of an existing Notification.
+ *
+ * @param notification The notification from which to copy options.
+ */
+ @SuppressWarnings("deprecation")
+ public CarExtender(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT < 21) {
+ return;
+ }
+
+ Bundle carBundle = getExtras(notification) == null
+ ? null : getExtras(notification).getBundle(EXTRA_CAR_EXTENDER);
+ if (carBundle != null) {
+ mLargeIcon = carBundle.getParcelable(EXTRA_LARGE_ICON);
+ mColor = carBundle.getInt(EXTRA_COLOR, NotificationCompat.COLOR_DEFAULT);
+
+ Bundle b = carBundle.getBundle(EXTRA_CONVERSATION);
+ mUnreadConversation = getUnreadConversationFromBundle(b);
+ }
+ }
+
+ @RequiresApi(21)
+ @SuppressWarnings("deprecation")
+ private static UnreadConversation getUnreadConversationFromBundle(@Nullable Bundle b) {
+ if (b == null) {
+ return null;
+ }
+ Parcelable[] parcelableMessages = b.getParcelableArray(KEY_MESSAGES);
+ String[] messages = null;
+ if (parcelableMessages != null) {
+ String[] tmp = new String[parcelableMessages.length];
+ boolean success = true;
+ for (int i = 0; i < tmp.length; i++) {
+ if (!(parcelableMessages[i] instanceof Bundle)) {
+ success = false;
+ break;
+ }
+ tmp[i] = ((Bundle) parcelableMessages[i]).getString(KEY_TEXT);
+ if (tmp[i] == null) {
+ success = false;
+ break;
+ }
+ }
+ if (success) {
+ messages = tmp;
+ } else {
+ return null;
+ }
+ }
+
+ PendingIntent onRead = b.getParcelable(KEY_ON_READ);
+ PendingIntent onReply = b.getParcelable(KEY_ON_REPLY);
+
+ android.app.RemoteInput remoteInput = b.getParcelable(KEY_REMOTE_INPUT);
+
+ String[] participants = b.getStringArray(KEY_PARTICIPANTS);
+ if (participants == null || participants.length != 1) {
+ return null;
+ }
+
+ RemoteInput remoteInputCompat = remoteInput != null
+ ? new RemoteInput(Api20Impl.getResultKey(remoteInput),
+ Api20Impl.getLabel(remoteInput),
+ Api20Impl.getChoices(remoteInput),
+ Api20Impl.getAllowFreeFormInput(remoteInput),
+ Build.VERSION.SDK_INT >= 29
+ ? Api29Impl.getEditChoicesBeforeSending(remoteInput)
+ : RemoteInput.EDIT_CHOICES_BEFORE_SENDING_AUTO,
+ Api20Impl.getExtras(remoteInput),
+ null /* allowedDataTypes */)
+ : null;
+
+ return new UnreadConversation(messages, remoteInputCompat, onReply,
+ onRead, participants, b.getLong(KEY_TIMESTAMP));
+ }
+
+ @RequiresApi(21)
+ private static Bundle getBundleForUnreadConversation(@NonNull UnreadConversation uc) {
+ Bundle b = new Bundle();
+ String author = null;
+ if (uc.getParticipants() != null && uc.getParticipants().length > 1) {
+ author = uc.getParticipants()[0];
+ }
+ Parcelable[] messages = new Parcelable[uc.getMessages().length];
+ for (int i = 0; i < messages.length; i++) {
+ Bundle m = new Bundle();
+ m.putString(KEY_TEXT, uc.getMessages()[i]);
+ m.putString(KEY_AUTHOR, author);
+ messages[i] = m;
+ }
+ b.putParcelableArray(KEY_MESSAGES, messages);
+ RemoteInput remoteInputCompat = uc.getRemoteInput();
+ if (remoteInputCompat != null) {
+ android.app.RemoteInput.Builder builder = Api20Impl.createBuilder(
+ remoteInputCompat.getResultKey());
+ Api20Impl.setLabel(builder, remoteInputCompat.getLabel());
+ Api20Impl.setChoices(builder, remoteInputCompat.getChoices());
+ Api20Impl.setAllowFreeFormInput(builder, remoteInputCompat.getAllowFreeFormInput());
+ Api20Impl.addExtras(builder, remoteInputCompat.getExtras());
+
+ android.app.RemoteInput remoteInput = Api20Impl.build(builder);
+ b.putParcelable(KEY_REMOTE_INPUT, Api20Impl.castToParcelable(remoteInput));
+ }
+ b.putParcelable(KEY_ON_REPLY, uc.getReplyPendingIntent());
+ b.putParcelable(KEY_ON_READ, uc.getReadPendingIntent());
+ b.putStringArray(KEY_PARTICIPANTS, uc.getParticipants());
+ b.putLong(KEY_TIMESTAMP, uc.getLatestTimestamp());
+ return b;
+ }
+
+ /**
+ * Apply car extensions to a notification that is being built. This is typically called by
+ * the {@link androidx.core.app.NotificationCompat.Builder#extend(NotificationCompat.Extender)}
+ * method of {@link NotificationCompat.Builder}.
+ */
+ @Override
+ @NonNull
+ public NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
+ if (Build.VERSION.SDK_INT < 21) {
+ return builder;
+ }
+
+ Bundle carExtensions = new Bundle();
+
+ if (mLargeIcon != null) {
+ carExtensions.putParcelable(EXTRA_LARGE_ICON, mLargeIcon);
+ }
+ if (mColor != NotificationCompat.COLOR_DEFAULT) {
+ carExtensions.putInt(EXTRA_COLOR, mColor);
+ }
+
+ if (mUnreadConversation != null) {
+ Bundle b = getBundleForUnreadConversation(mUnreadConversation);
+ carExtensions.putBundle(EXTRA_CONVERSATION, b);
+ }
+
+ builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions);
+ return builder;
+ }
+
+ /**
+ * Sets the accent color to use when Android Auto presents the notification.
+ *
+ * Android Auto uses the color set with {@link androidx.core.app.NotificationCompat.Builder#setColor(int)}
+ * to accent the displayed notification. However, not all colors are acceptable in an
+ * automotive setting. This method can be used to override the color provided in the
+ * notification in such a situation.
+ */
+ public @NonNull CarExtender setColor(@ColorInt int color) {
+ mColor = color;
+ return this;
+ }
+
+ /**
+ * Gets the accent color.
+ *
+ * @see #setColor
+ */
+ @ColorInt
+ public int getColor() {
+ return mColor;
+ }
+
+ /**
+ * Sets the large icon of the car notification.
+ *
+ * If no large icon is set in the extender, Android Auto will display the icon
+ * specified by {@link androidx.core.app.NotificationCompat.Builder#setLargeIcon(android.graphics.Bitmap)}
+ *
+ * @param largeIcon The large icon to use in the car notification.
+ * @return This object for method chaining.
+ */
+ public @NonNull CarExtender setLargeIcon(@Nullable Bitmap largeIcon) {
+ mLargeIcon = largeIcon;
+ return this;
+ }
+
+ /**
+ * Gets the large icon used in this car notification, or null if no icon has been set.
+ *
+ * @return The large icon for the car notification.
+ * @see CarExtender#setLargeIcon
+ */
+ public @Nullable Bitmap getLargeIcon() {
+ return mLargeIcon;
+ }
+
+ /**
+ * Sets the unread conversation in a message notification.
+ *
+ * @param unreadConversation The unread part of the conversation this notification conveys.
+ * @return This object for method chaining.
+ *
+ * @deprecated {@link UnreadConversation} is no longer supported. Use {@link MessagingStyle}
+ * instead.
+ */
+ @Deprecated
+ @NonNull
+ public CarExtender setUnreadConversation(@Nullable UnreadConversation unreadConversation) {
+ mUnreadConversation = unreadConversation;
+ return this;
+ }
+
+ /**
+ * Returns the unread conversation conveyed by this notification.
+ * @see #setUnreadConversation(UnreadConversation)
+ *
+ * @deprecated {@link UnreadConversation} is no longer supported. Use {@link MessagingStyle}
+ * instead.
+ */
+ @Deprecated
+ public @Nullable UnreadConversation getUnreadConversation() {
+ return mUnreadConversation;
+ }
+
+ /**
+ * A class which holds the unread messages from a conversation.
+ *
+ * @deprecated {@link UnreadConversation} is no longer supported. Use {@link MessagingStyle}
+ * instead.
+ */
+ @Deprecated
+ public static class UnreadConversation {
+ private final String[] mMessages;
+ private final RemoteInput mRemoteInput;
+ private final PendingIntent mReplyPendingIntent;
+ private final PendingIntent mReadPendingIntent;
+ private final String[] mParticipants;
+ private final long mLatestTimestamp;
+
+ UnreadConversation(@Nullable String[] messages, @Nullable RemoteInput remoteInput,
+ @Nullable PendingIntent replyPendingIntent,
+ @Nullable PendingIntent readPendingIntent,
+ @Nullable String[] participants, long latestTimestamp) {
+ mMessages = messages;
+ mRemoteInput = remoteInput;
+ mReadPendingIntent = readPendingIntent;
+ mReplyPendingIntent = replyPendingIntent;
+ mParticipants = participants;
+ mLatestTimestamp = latestTimestamp;
+ }
+
+ /**
+ * Gets the list of messages conveyed by this notification.
+ */
+ public @Nullable String[] getMessages() {
+ return mMessages;
+ }
+
+ /**
+ * Gets the remote input that will be used to convey the response to a message list, or
+ * null if no such remote input exists.
+ */
+ public @Nullable RemoteInput getRemoteInput() {
+ return mRemoteInput;
+ }
+
+ /**
+ * Gets the pending intent that will be triggered when the user replies to this
+ * notification.
+ */
+ public @Nullable PendingIntent getReplyPendingIntent() {
+ return mReplyPendingIntent;
+ }
+
+ /**
+ * Gets the pending intent that Android Auto will send after it reads aloud all messages
+ * in this object's message list.
+ */
+ public @Nullable PendingIntent getReadPendingIntent() {
+ return mReadPendingIntent;
+ }
+
+ /**
+ * Gets the participants in the conversation.
+ */
+ public @Nullable String[] getParticipants() {
+ return mParticipants;
+ }
+
+ /**
+ * Gets the firs participant in the conversation.
+ */
+ public @Nullable String getParticipant() {
+ return mParticipants.length > 0 ? mParticipants[0] : null;
+ }
+
+ /**
+ * Gets the timestamp of the conversation.
+ */
+ public long getLatestTimestamp() {
+ return mLatestTimestamp;
+ }
+
+ /**
+ * Builder class for {@link CarExtender.UnreadConversation} objects.
+ */
+ public static class Builder {
+ private final List
+ * To create a notification with a TV extension:
+ * TV extensions can be accessed on an existing notification by using the
+ * {@code TvExtender(Notification)} constructor, and then using the {@code get} methods
+ * to access values.
+ *
+ * Note that prior to {@link Build.VERSION_CODES#O} this field has no effect.
+ */
+ public static final class TvExtender implements Extender {
+ private static final String TAG = "TvExtender";
+
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ static final String EXTRA_TV_EXTENDER = "android.tv.EXTENSIONS";
+
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ private static final String EXTRA_FLAGS = "flags";
+
+ static final String EXTRA_CONTENT_INTENT = "content_intent";
+ static final String EXTRA_DELETE_INTENT = "delete_intent";
+ static final String EXTRA_CHANNEL_ID = "channel_id";
+ static final String EXTRA_SUPPRESS_SHOW_OVER_APPS = "suppressShowOverApps";
+
+ // Flags bitwise-ored to mFlags
+ private static final int FLAG_AVAILABLE_ON_TV = 0x1;
+
+ private int mFlags;
+ private String mChannelId;
+ private PendingIntent mContentIntent;
+ private PendingIntent mDeleteIntent;
+ private boolean mSuppressShowOverApps;
+
+ /**
+ * Create a {@link TvExtender} with default options.
+ */
+ public TvExtender() {
+ mFlags = FLAG_AVAILABLE_ON_TV;
+ }
+
+ /**
+ * Create a {@link TvExtender} from the TvExtender options of an existing Notification.
+ *
+ * @param notif The notification from which to copy options.
+ */
+ public TvExtender(@NonNull Notification notif) {
+ // TvExtender was introduced in API level 26; note that before API level 26, the extras
+ // added by TvExtender are not expected to be used; thus, we avoid setting them to save
+ // memory.
+ if (Build.VERSION.SDK_INT < 26) {
+ return;
+ }
+
+ Bundle tvBundle = notif.extras == null
+ ? null : notif.extras.getBundle(EXTRA_TV_EXTENDER);
+ if (tvBundle != null) {
+ mFlags = tvBundle.getInt(EXTRA_FLAGS);
+ mChannelId = tvBundle.getString(EXTRA_CHANNEL_ID);
+ mSuppressShowOverApps = tvBundle.getBoolean(EXTRA_SUPPRESS_SHOW_OVER_APPS);
+ mContentIntent = (PendingIntent) tvBundle.getParcelable(EXTRA_CONTENT_INTENT);
+ mDeleteIntent = (PendingIntent) tvBundle.getParcelable(EXTRA_DELETE_INTENT);
+ }
+ }
+
+ /**
+ * Apply a TV extension to a notification that is being built. This is typically called by
+ * the
+ * {@link androidx.core.app.NotificationCompat.Builder#extend(NotificationCompat.Extender)}
+ * method of {@link NotificationCompat.Builder}.
+ */
+ @Override
+ @NonNull
+ public NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
+ // TvExtender was introduced in API level 26; note that before API level 26, the extras
+ // added by TvExtender are not expected to be used; thus, we avoid setting them to save
+ // memory.
+ if (Build.VERSION.SDK_INT < 26) {
+ return builder;
+ }
+
+ Bundle tvExtensions = new Bundle();
+
+ tvExtensions.putInt(EXTRA_FLAGS, mFlags);
+ tvExtensions.putString(EXTRA_CHANNEL_ID, mChannelId);
+ tvExtensions.putBoolean(EXTRA_SUPPRESS_SHOW_OVER_APPS, mSuppressShowOverApps);
+ if (mContentIntent != null) {
+ tvExtensions.putParcelable(EXTRA_CONTENT_INTENT, mContentIntent);
+ }
+
+ if (mDeleteIntent != null) {
+ tvExtensions.putParcelable(EXTRA_DELETE_INTENT, mDeleteIntent);
+ }
+
+ // Extras was added in API level 19.
+ builder.getExtras().putBundle(EXTRA_TV_EXTENDER, tvExtensions);
+ return builder;
+ }
+
+ /**
+ * Returns true if this notification should be shown on TV. This method return true
+ * if the notification was extended with a TvExtender.
+ */
+ public boolean isAvailableOnTv() {
+ return (mFlags & FLAG_AVAILABLE_ON_TV) != 0;
+ }
+
+ /**
+ * Specifies the channel the notification should be delivered on when shown on TV.
+ * It can be different from the channel that the notification is delivered to when
+ * posting on a non-TV device.
+ *
+ * @param channelId The channelId to use in the tv notification.
+ * @return The object for method chaining.
+ */
+ public @NonNull TvExtender setChannelId(@Nullable String channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ /**
+ * Returns the id of the channel this notification posts to on TV.
+ */
+ public @Nullable String getChannelId() {
+ return mChannelId;
+ }
+
+ /**
+ * Supplies a {@link PendingIntent} to be sent when the notification is selected on TV.
+ * If provided, it is used instead of the content intent specified
+ * at the level of Notification.
+ */
+ public @NonNull TvExtender setContentIntent(@Nullable PendingIntent intent) {
+ mContentIntent = intent;
+ return this;
+ }
+
+ /**
+ * Returns the TV-specific content intent. If this method returns null, the
+ * main content intent on the notification should be used.
+ *
+ * @see {@link Notification#contentIntent}
+ */
+ public @Nullable PendingIntent getContentIntent() {
+ return mContentIntent;
+ }
+
+ /**
+ * Supplies a {@link PendingIntent} to send when the notification is cleared explicitly
+ * by the user on TV. If provided, it is used instead of the delete intent specified
+ * at the level of Notification.
+ */
+ public @NonNull TvExtender setDeleteIntent(@Nullable PendingIntent intent) {
+ mDeleteIntent = intent;
+ return this;
+ }
+
+ /**
+ * Returns the TV-specific delete intent. If this method returns null, the
+ * main delete intent on the notification should be used.
+ *
+ * @see {@link Notification#deleteIntent}
+ */
+ public @Nullable PendingIntent getDeleteIntent() {
+ return mDeleteIntent;
+ }
+
+ /**
+ * Specifies whether this notification should suppress showing a message over top of apps
+ * outside of the launcher.
+ */
+ public @NonNull TvExtender setSuppressShowOverApps(boolean suppress) {
+ mSuppressShowOverApps = suppress;
+ return this;
+ }
+
+ /**
+ * Returns true if this notification should not show messages over top of apps
+ * outside of the launcher.
+ */
+ public boolean isSuppressShowOverApps() {
+ return mSuppressShowOverApps;
+ }
+ }
+
+ /**
+ * Encapsulates the information needed to display a notification as a bubble.
+ *
+ * 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(IconCompat)} and an expanded state which is populated
+ * via {@link BubbleMetadata.Builder#setIntent(PendingIntent)}. If the app creating the bubble is not in the foreground this flag has no effect. Generally this flag should only be set if the user has performed an action to request
+ * or create a bubble. Apps sending bubbles may set this flag so that the bubble is posted without
+ * the associated notification in the notification shade. Apps sending bubbles can only apply this flag when the app is in the foreground,
+ * otherwise the flag is not respected. The app is considered foreground if it is visible
+ * and on the screen, note that a foreground service does not qualify. Generally this flag should only be set by the app if the user has performed an
+ * action to request or create a bubble, or if the user has seen the content in the
+ * notification and the notification is no longer relevant. The system will also update this flag with The shortcut icon will be used to represent the bubble when it is collapsed. 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. 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. 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. 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. The intent
+ * should point to a resizable activity. 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. It is recommended to use an {@link Icon} of type {@link Icon#TYPE_URI}
+ * or {@link Icon#TYPE_URI_ADAPTIVE_BITMAP}
+ * If {@link #setDesiredHeightResId(int)} was previously called on this builder, the
+ * previous value set will be cleared after calling this method, and this value will
+ * be used instead.
+ */
+ @NonNull
+ public BubbleMetadata.Builder setDesiredHeight(@Dimension(unit = DP) int height) {
+ mDesiredHeight = Math.max(height, 0);
+ mDesiredHeightResId = 0;
+ return this;
+ }
+
+ /**
+ * Sets the desired height via resId for the app content defined by
+ * {@link #setIntent(PendingIntent)}, 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.
+ *
+ * If {@link #setDesiredHeight(int)} was previously called on this builder, the
+ * previous value set will be cleared after calling this method, and this value will
+ * be used instead.
+ */
+ @NonNull
+ public BubbleMetadata.Builder setDesiredHeightResId(@DimenRes int heightResId) {
+ mDesiredHeightResId = heightResId;
+ mDesiredHeight = 0;
+ return this;
+ }
+
+ /**
+ * 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.
+ *
+ * If the app creating the bubble is not in the foreground this flag has no effect.
+ * Generally this flag should only be set if the user has performed an action to
+ * request or create a bubble. If the app posting the bubble is not in the foreground this flag has no effect.
+ * Generally this flag should only be set if the user has performed an action to
+ * request or create a bubble, or if the user has seen the content in the notification
+ * and the notification is no longer relevant. Will throw {@link NullPointerException} if required fields have not been set
+ * on this builder. Some notifications can be bridged to other devices for remote display.
+ * If this hint is set, it is recommend that this notification not be bridged.
+ */
+ public static boolean getLocalOnly(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 20) {
+ return (notification.flags & Notification.FLAG_LOCAL_ONLY) != 0;
+ } else {
+ return notification.extras.getBoolean(NotificationCompatExtras.EXTRA_LOCAL_ONLY);
+ }
+ }
+
+ /**
+ * Get the key used to group this notification into a cluster or stack
+ * with other notifications on devices which support such rendering.
+ */
+ public static @Nullable String getGroup(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 20) {
+ return Api20Impl.getGroup(notification);
+ } else {
+ return notification.extras.getString(NotificationCompatExtras.EXTRA_GROUP_KEY);
+ }
+ }
+
+ /** Get the value provided to {@link Builder#setShowWhen(boolean)} */
+ public static boolean getShowWhen(@NonNull Notification notification) {
+ // NOTE: This field can be set since API 17, but is impossible to extract from the
+ // constructed Notification until API 19 when it was moved to the extras.
+ return notification.extras.getBoolean(EXTRA_SHOW_WHEN);
+ }
+
+ /** Get the value provided to {@link Builder#setUsesChronometer(boolean)} */
+ public static boolean getUsesChronometer(@NonNull Notification notification) {
+ // NOTE: This field can be set since API 16, but is impossible to extract from the
+ // constructed Notification until API 19 when it was moved to the extras.
+ return notification.extras.getBoolean(EXTRA_SHOW_CHRONOMETER);
+ }
+
+ /** Get the value provided to {@link Builder#setOnlyAlertOnce(boolean)} */
+ public static boolean getOnlyAlertOnce(@NonNull Notification notification) {
+ return (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0;
+ }
+
+ /** Get the value provided to {@link Builder#setAutoCancel(boolean)} */
+ public static boolean getAutoCancel(@NonNull Notification notification) {
+ return (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0;
+ }
+
+ /** Get the value provided to {@link Builder#setOngoing(boolean)} */
+ public static boolean getOngoing(@NonNull Notification notification) {
+ return (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0;
+ }
+
+ /** Get the value provided to {@link Builder#setColor(int)} */
+ public static int getColor(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return notification.color;
+ } else {
+ return COLOR_DEFAULT;
+ }
+ }
+
+ /** Get the value provided to {@link Builder#setVisibility(int)} */
+ public static @NotificationVisibility int getVisibility(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return notification.visibility;
+ } else {
+ return VISIBILITY_PRIVATE;
+ }
+ }
+
+ /** Get the value provided to {@link Builder#setVisibility(int)} */
+ public static @Nullable Notification getPublicVersion(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return notification.publicVersion;
+ } else {
+ return null;
+ }
+ }
+
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ static boolean getHighPriority(@NonNull Notification notification) {
+ return (notification.flags & Notification.FLAG_HIGH_PRIORITY) != 0;
+ }
+
+ /**
+ * Get whether this notification to be the group summary for a group of notifications.
+ * Grouped notifications may display in a cluster or stack on devices which
+ * support such rendering. Requires a group key also be set using {@link Builder#setGroup}.
+ * @return Whether this notification is a group summary.
+ */
+ public static boolean isGroupSummary(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 20) {
+ return (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
+ } else {
+ return notification.extras.getBoolean(NotificationCompatExtras.EXTRA_GROUP_SUMMARY);
+ }
+ }
+
+ /**
+ * Get a sort key that orders this notification among other notifications from the
+ * same package. This can be useful if an external sort was already applied and an app
+ * would like to preserve this. Notifications will be sorted lexicographically using this
+ * value, although providing different priorities in addition to providing sort key may
+ * cause this value to be ignored.
+ *
+ * This sort key can also be used to order members of a notification group. See
+ * {@link Builder#setGroup}.
+ *
+ * @see String#compareTo(String)
+ */
+ public static @Nullable String getSortKey(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 20) {
+ return Api20Impl.getSortKey(notification);
+ } else {
+ return notification.extras.getString(NotificationCompatExtras.EXTRA_SORT_KEY);
+ }
+ }
+
+ /**
+ * @return the ID of the channel this notification posts to.
+ */
+ public static @Nullable String getChannelId(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.getChannelId(notification);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the time at which this notification should be canceled by the system, if it's not
+ * canceled already.
+ */
+ public static long getTimeoutAfter(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.getTimeoutAfter(notification);
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Returns what icon should be shown for this notification if it is being displayed in a
+ * Launcher that supports badging. Will be one of {@link #BADGE_ICON_NONE},
+ * {@link #BADGE_ICON_SMALL}, or {@link #BADGE_ICON_LARGE}.
+ */
+ public static int getBadgeIconType(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.getBadgeIconType(notification);
+ } else {
+ return BADGE_ICON_NONE;
+ }
+ }
+
+ /**
+ * Returns the {@link ShortcutInfoCompat#getId() id} that this
+ * notification supersedes, if any.
+ */
+ public static @Nullable String getShortcutId(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.getShortcutId(notification);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the settings text provided to {@link Builder#setSettingsText(CharSequence)}.
+ */
+ public static @Nullable CharSequence getSettingsText(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.getSettingsText(notification);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the {@link LocusIdCompat} associated with this notification.
+ *
+ * Used by the Android system to correlate objects (such as
+ * {@link androidx.core.content.pm.ShortcutInfoCompat} and
+ * {@link android.view.contentcapture.ContentCaptureContext}).
+ */
+ @Nullable
+ public static LocusIdCompat getLocusId(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ LocusId locusId = Api29Impl.getLocusId(notification);
+ return locusId == null ? null : LocusIdCompat.toLocusIdCompat(locusId);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns which type of notifications in a group are responsible for audibly alerting the
+ * user. See {@link #GROUP_ALERT_ALL}, {@link #GROUP_ALERT_CHILDREN},
+ * {@link #GROUP_ALERT_SUMMARY}.
+ */
+ @GroupAlertBehavior
+ public static int getGroupAlertBehavior(@NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.getGroupAlertBehavior(notification);
+ } else {
+ return GROUP_ALERT_ALL;
+ }
+ }
+
+ /**
+ * Returns whether the platform is allowed (by the app developer) to generate contextual actions
+ * for this notification.
+ */
+ public static boolean getAllowSystemGeneratedContextualActions(
+ @NonNull Notification notification) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ return Api29Impl.getAllowSystemGeneratedContextualActions(notification);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Reduces the size of a provided {@code icon} if it's larger than the maximum allowed
+ * for a notification large icon; returns the resized icon. Note that the framework does this
+ * scaling automatically starting from API 27.
+ */
+ public static @Nullable Bitmap reduceLargeIconSize(@NonNull Context context,
+ @Nullable Bitmap icon) {
+ if (icon == null || Build.VERSION.SDK_INT >= 27) {
+ return icon;
+ }
+
+ Resources res = context.getResources();
+ int maxWidth =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_width);
+ int maxHeight =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_height);
+ if (icon.getWidth() <= maxWidth && icon.getHeight() <= maxHeight) {
+ return icon;
+ }
+
+ double scale = Math.min(
+ maxWidth / (double) Math.max(1, icon.getWidth()),
+ maxHeight / (double) Math.max(1, icon.getHeight()));
+ return Bitmap.createScaledBitmap(
+ icon,
+ (int) Math.ceil(icon.getWidth() * scale),
+ (int) Math.ceil(icon.getHeight() * scale),
+ true /* filtered */);
+ }
+
+ /** @deprecated This type should not be instantiated as it contains only static methods. */
+ @Deprecated
+ @SuppressWarnings("PrivateConstructorForUtilityClass")
+ public NotificationCompat() {
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification} methods which
+ * were added in API 20; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(20)
+ static class Api20Impl {
+ private Api20Impl() { }
+
+ @DoNotInline
+ static boolean getAllowFreeFormInput(android.app.RemoteInput remoteInput) {
+ return remoteInput.getAllowFreeFormInput();
+ }
+
+ @DoNotInline
+ static CharSequence[] getChoices(android.app.RemoteInput remoteInput) {
+ return remoteInput.getChoices();
+ }
+
+ @DoNotInline
+ static CharSequence getLabel(android.app.RemoteInput remoteInput) {
+ return remoteInput.getLabel();
+ }
+
+ @DoNotInline
+ static String getResultKey(android.app.RemoteInput remoteInput) {
+ return remoteInput.getResultKey();
+ }
+
+ @DoNotInline
+ static android.app.RemoteInput[] getRemoteInputs(Notification.Action action) {
+ return action.getRemoteInputs();
+ }
+
+ @DoNotInline
+ static String getSortKey(Notification notification) {
+ return notification.getSortKey();
+ }
+
+ @DoNotInline
+ static String getGroup(Notification notification) {
+ return notification.getGroup();
+ }
+
+ @DoNotInline
+ static Bundle getExtras(Notification.Action action) {
+ return action.getExtras();
+ }
+
+ @DoNotInline
+ static Bundle getExtras(android.app.RemoteInput remoteInput) {
+ return remoteInput.getExtras();
+ }
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification} methods which
+ * were added in API 23; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(23)
+ static class Api23Impl {
+ private Api23Impl() { }
+
+ @DoNotInline
+ static Icon getIcon(Notification.Action action) {
+ return action.getIcon();
+ }
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification} methods which
+ * were added in API 24; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(24)
+ static class Api24Impl {
+ private Api24Impl() { }
+
+ @DoNotInline
+ static boolean getAllowGeneratedReplies(Notification.Action action) {
+ return action.getAllowGeneratedReplies();
+ }
+
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification} methods which
+ * were added in API 26; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(26)
+ static class Api26Impl {
+ private Api26Impl() { }
+
+ @DoNotInline
+ static int getGroupAlertBehavior(Notification notification) {
+ return notification.getGroupAlertBehavior();
+ }
+
+ @DoNotInline
+ static CharSequence getSettingsText(Notification notification) {
+ return notification.getSettingsText();
+ }
+
+ @DoNotInline
+ static String getShortcutId(Notification notification) {
+ return notification.getShortcutId();
+ }
+
+ @DoNotInline
+ static int getBadgeIconType(Notification notification) {
+ return notification.getBadgeIconType();
+ }
+
+ @DoNotInline
+ static long getTimeoutAfter(Notification notification) {
+ return notification.getTimeoutAfter();
+ }
+
+ @DoNotInline
+ static String getChannelId(Notification notification) {
+ return notification.getChannelId();
+ }
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification} methods which
+ * were added in API 28; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(28)
+ static class Api28Impl {
+ private Api28Impl() { }
+
+ @DoNotInline
+ static int getSemanticAction(Notification.Action action) {
+ return action.getSemanticAction();
+ }
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification} methods which
+ * were added in API 29; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(29)
+ static class Api29Impl {
+ private Api29Impl() { }
+
+ @DoNotInline
+ static boolean getAllowSystemGeneratedContextualActions(Notification notification) {
+ return notification.getAllowSystemGeneratedContextualActions();
+ }
+
+ @DoNotInline
+ static LocusId getLocusId(Notification notification) {
+ return notification.getLocusId();
+ }
+
+ @DoNotInline
+ static boolean isContextual(Notification.Action action) {
+ return action.isContextual();
+ }
+
+ @DoNotInline
+ static int getEditChoicesBeforeSending(android.app.RemoteInput remoteInput) {
+ return remoteInput.getEditChoicesBeforeSending();
+ }
+
+ @DoNotInline
+ static Notification.BubbleMetadata getBubbleMetadata(Notification notification) {
+ return notification.getBubbleMetadata();
+ }
+ }
+
+ /**
+ * A class for wrapping calls to {@link Notification} methods which
+ * were added in API 31; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(31)
+ static class Api31Impl {
+ private Api31Impl() { }
+
+ @DoNotInline
+ static boolean isAuthenticationRequired(Notification.Action action) {
+ return action.isAuthenticationRequired();
+ }
+
+ }
+}
diff --git a/app/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/app/src/main/java/androidx/core/app/NotificationCompatBuilder.java
new file mode 100644
index 00000000..e8c7602a
--- /dev/null
+++ b/app/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -0,0 +1,831 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.app;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import static androidx.core.app.NotificationCompat.DEFAULT_SOUND;
+import static androidx.core.app.NotificationCompat.DEFAULT_VIBRATE;
+import static androidx.core.app.NotificationCompat.FLAG_GROUP_SUMMARY;
+import static androidx.core.app.NotificationCompat.GROUP_ALERT_ALL;
+import static androidx.core.app.NotificationCompat.GROUP_ALERT_CHILDREN;
+import static androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.LocusId;
+import android.graphics.drawable.Icon;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import android.widget.RemoteViews;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+import androidx.core.graphics.drawable.IconCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Wrapper around {@link Notification.Builder} that works in a backwards compatible way.
+ *
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccessor {
+ private final Context mContext;
+ private final Notification.Builder mBuilder;
+ private final NotificationCompat.Builder mBuilderCompat;
+
+ private RemoteViews mContentView;
+ private RemoteViews mBigContentView;
+ private final List *Note for these schemas, the path portion of the URI must exist in the contacts
+ * database in their appropriate column, otherwise the reference should be discarded.
+ */
+ @Nullable
+ public String getUri() {
+ return mUri;
+ }
+
+ /**
+ * Returns the key for this {@link Person} or {@code null} if no key was provided. This is
+ * provided as a unique identifier between other {@link Person}s.
+ */
+ @Nullable
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * Returns whether or not this {@link Person} is a machine rather than a human. Used primarily
+ * to identify automated tooling.
+ */
+ public boolean isBot() {
+ return mIsBot;
+ }
+
+ /**
+ * Returns whether or not this {@link Person} is important to the user of this device with
+ * regards to how frequently they interact.
+ */
+ public boolean isImportant() {
+ return mIsImportant;
+ }
+
+ /**
+ * @return the URI associated with this person, or "name:mName" otherwise
+ */
+ @NonNull
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public String resolveToLegacyUri() {
+ if (mUri != null) {
+ return mUri;
+ }
+ if (mName != null) {
+ return "name:" + mName;
+ }
+ return "";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object otherObject) {
+ if (otherObject == null) {
+ return false;
+ }
+
+ if (!(otherObject instanceof Person)) {
+ return false;
+ }
+
+ Person otherPerson = (Person) otherObject;
+
+ // If a unique ID was provided, use it
+ String key1 = getKey();
+ String key2 = otherPerson.getKey();
+ if (key1 != null || key2 != null) {
+ return Objects.equals(key1, key2);
+ }
+
+ // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
+ String name1 = Objects.toString(getName());
+ String name2 = Objects.toString(otherPerson.getName());
+
+ // Fallback: Compare field-by-field
+ return
+ Objects.equals(name1, name2)
+ && Objects.equals(getUri(), otherPerson.getUri())
+ && Objects.equals(isBot(), otherPerson.isBot())
+ && Objects.equals(isImportant(), otherPerson.isImportant());
+ }
+
+ @Override
+ public int hashCode() {
+ // If a unique ID was provided, use it
+ String key = getKey();
+ if (key != null) {
+ return key.hashCode();
+ }
+
+ // Fallback: Use hash code for individual fields
+ return Objects.hash(getName(), getUri(), isBot(), isImportant());
+ }
+
+ /** Builder for the immutable {@link Person} class. */
+ public static class Builder {
+ @Nullable CharSequence mName;
+ @Nullable IconCompat mIcon;
+ @Nullable String mUri;
+ @Nullable String mKey;
+ boolean mIsBot;
+ boolean mIsImportant;
+
+ /** Creates a new, empty {@link Builder}. */
+ public Builder() { }
+
+ Builder(Person person) {
+ mName = person.mName;
+ mIcon = person.mIcon;
+ mUri = person.mUri;
+ mKey = person.mKey;
+ mIsBot = person.mIsBot;
+ mIsImportant = person.mIsImportant;
+ }
+
+ /**
+ * Give this {@link Person} a name to use for display. This can be, for example, a full
+ * name, nickname, username, etc.
+ */
+ @NonNull
+ public Builder setName(@Nullable CharSequence name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Set an icon for this {@link Person}.
+ *
+ * The system will prefer this icon over any images that are resolved from
+ * {@link #setUri(String)}.
+ */
+ @NonNull
+ public Builder setIcon(@Nullable IconCompat icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Set a URI for this {@link Person} which can be any of the following:
+ * *Note for these schemas, the path portion of the URI must exist in the contacts
+ * database in their appropriate column, otherwise the reference will be discarded.
+ */
+ @NonNull
+ public Builder setUri(@Nullable String uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Set a unique identifier for this {@link Person}. This is especially useful if the
+ * {@link #setName(CharSequence)} value isn't unique. This value is preferred for
+ * identification, but if it's not provided, the person's name will be used in its place.
+ */
+ @NonNull
+ public Builder setKey(@Nullable String key) {
+ mKey = key;
+ return this;
+ }
+
+ /**
+ * Sets whether or not this {@link Person} represents a machine rather than a human. This is
+ * used primarily for testing and automated tooling.
+ */
+ @NonNull
+ public Builder setBot(boolean bot) {
+ mIsBot = bot;
+ return this;
+ }
+
+ /**
+ * Sets whether this is an important person. Use this method to denote users who frequently
+ * interact with the user of this device when {@link #setUri(String)} isn't provided with
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, and instead with
+ * the {@code mailto:} or {@code tel:} schemas.
+ */
+ @NonNull
+ public Builder setImportant(boolean important) {
+ mIsImportant = important;
+ return this;
+ }
+
+ /** Creates and returns the {@link Person} this builder represents. */
+ @NonNull
+ public Person build() {
+ return new Person(this);
+ }
+ }
+
+ @RequiresApi(22)
+ static class Api22Impl {
+ private Api22Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static Person fromPersistableBundle(PersistableBundle bundle) {
+ return new Builder()
+ .setName(bundle.getString(NAME_KEY))
+ .setUri(bundle.getString(URI_KEY))
+ .setKey(bundle.getString(KEY_KEY))
+ .setBot(bundle.getBoolean(IS_BOT_KEY))
+ .setImportant(bundle.getBoolean(IS_IMPORTANT_KEY))
+ .build();
+ }
+
+ @DoNotInline
+ static PersistableBundle toPersistableBundle(Person person) {
+ PersistableBundle result = new PersistableBundle();
+ result.putString(NAME_KEY, person.mName != null ? person.mName.toString() : null);
+ result.putString(URI_KEY, person.mUri);
+ result.putString(KEY_KEY, person.mKey);
+ result.putBoolean(IS_BOT_KEY, person.mIsBot);
+ result.putBoolean(IS_IMPORTANT_KEY, person.mIsImportant);
+ return result;
+ }
+ }
+
+ @RequiresApi(28)
+ static class Api28Impl {
+ private Api28Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static Person fromAndroidPerson(android.app.Person person) {
+ return new Builder()
+ .setName(person.getName())
+ .setIcon(
+ (person.getIcon() != null)
+ ? IconCompat.createFromIcon(person.getIcon())
+ : null)
+ .setUri(person.getUri())
+ .setKey(person.getKey())
+ .setBot(person.isBot())
+ .setImportant(person.isImportant())
+ .build();
+ }
+
+ @SuppressWarnings("deprecation")
+ @DoNotInline
+ static android.app.Person toAndroidPerson(Person person, Context context) {
+ return new android.app.Person.Builder()
+ .setName(person.getName())
+ .setIcon((person.getIcon() != null) ? person.getIcon().toIcon(context) : null)
+ .setUri(person.getUri())
+ .setKey(person.getKey())
+ .setBot(person.isBot())
+ .setImportant(person.isImportant())
+ .build();
+ }
+ }
+}
null for messages by the current user, in which case
+ * the platform will insert {@link MessagingStyle#getUserDisplayName()}.
+ * Should be unique amongst all individuals in the conversation, and should be
+ * consistent during re-posts of the notification.
+ *
+ * @deprecated Use the alternative constructor instead.
+ */
+ @Deprecated
+ public Message(@Nullable CharSequence text, long timestamp,
+ @Nullable CharSequence sender) {
+ this(text, timestamp, new Person.Builder().setName(sender).build());
+ }
+
+ /**
+ * Sets a binary blob of data and an associated MIME type for a message. In the case
+ * where the platform doesn't support the MIME type, the original text provided in the
+ * constructor will be used.
+ *
+ * @param dataMimeType The MIME type of the content. See
+ * {@link android.graphics.ImageDecoder#isMimeTypeSupported(String)}
+ * for a list of supported image MIME types.
+ * @param dataUri The uri containing the content whose type is given by the MIME type.
+ *
+ *
+ *
+ * @return this object for method chaining
+ */
+ public @NonNull Message setData(@Nullable String dataMimeType, @Nullable Uri dataUri) {
+ mDataMimeType = dataMimeType;
+ mDataUri = dataUri;
+ return this;
+ }
+
+ /**
+ * Get the text to be used for this message, or the fallback text if a type and content
+ * Uri have been set
+ */
+ @Nullable
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /** Get the time at which this message arrived in ms since Unix epoch. */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /** Get the extras Bundle for this message. */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Get the text used to display the contact's name in the messaging experience
+ *
+ * @deprecated Use {@link #getPerson()}
+ */
+ @Deprecated
+ @Nullable
+ public CharSequence getSender() {
+ return mPerson == null ? null : mPerson.getName();
+ }
+
+ /** Returns the {@link Person} sender of this message. */
+ @Nullable
+ public Person getPerson() {
+ return mPerson;
+ }
+
+ /** Get the MIME type of the data pointed to by the URI. */
+ @Nullable
+ public String getDataMimeType() {
+ return mDataMimeType;
+ }
+
+ /**
+ * Get the the Uri pointing to the content of the message. Can be null, in which case
+ * {@see #getText()} is used.
+ */
+ @Nullable
+ public Uri getDataUri() {
+ return mDataUri;
+ }
+
+ private @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ if (mText != null) {
+ bundle.putCharSequence(KEY_TEXT, mText);
+ }
+ bundle.putLong(KEY_TIMESTAMP, mTimestamp);
+ if (mPerson != null) {
+ // We must add both as Frameworks depends on this extra directly in order to
+ // render properly.
+ bundle.putCharSequence(KEY_SENDER, mPerson.getName());
+
+ // Write person to native notification
+ if (Build.VERSION.SDK_INT >= 28) {
+ bundle.putParcelable(KEY_NOTIFICATION_PERSON,
+ Api28Impl.castToParcelable(mPerson.toAndroidPerson()));
+ } else {
+ bundle.putBundle(KEY_PERSON, mPerson.toBundle());
+ }
+ }
+ if (mDataMimeType != null) {
+ bundle.putString(KEY_DATA_MIME_TYPE, mDataMimeType);
+ }
+ if (mDataUri != null) {
+ bundle.putParcelable(KEY_DATA_URI, mDataUri);
+ }
+ if (mExtras != null) {
+ bundle.putBundle(KEY_EXTRAS_BUNDLE, mExtras);
+ }
+ return bundle;
+ }
+
+ @NonNull
+ static Bundle[] getBundleArrayForMessages(@NonNull List
+ * If the platform does not provide large-format notifications, this method has no effect. The
+ * user will always see the normal notification view.
+ *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior,
+ * like so:
+ *
+ * Notification notification = new NotificationCompat.Builder(mContext)
+ * .setSmallIcon(R.drawable.new_post)
+ * .setStyle(
+ * new Notification.CallStyle.forIncomingCall(caller, declineIntent, answerIntent))
+ * .build();
+ *
+ *
+ * Note that for CallStyle Notifications on API versions before 31 to be ranked as they are
+ * in API versions 31+, they must be associated with a foreground service. Additionally,
+ * CallStyle Notifications on API versions before 31 can achieve the similar ranking by marking
+ * the Notification as colorized, using {@link Builder#setColorized(boolean)}.
+ */
+ public static class CallStyle extends Style {
+
+ private static final String TEMPLATE_CLASS_NAME =
+ "androidx.core.app.NotificationCompat$CallStyle";
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ CALL_TYPE_UNKNOWN,
+ CALL_TYPE_INCOMING,
+ CALL_TYPE_ONGOING,
+ CALL_TYPE_SCREENING
+ })
+ public @interface CallType {
+ }
+
+ ;
+
+ // TODO: Replace these with the real CALL_TYPE constants, once they are available.
+ /**
+ * Unknown call type.
+ *
+ * See {@link #EXTRA_CALL_TYPE}.
+ */
+ public static final int CALL_TYPE_UNKNOWN = 0;
+
+ /**
+ * Call type for incoming calls.
+ *
+ * See {@link #EXTRA_CALL_TYPE}.
+ */
+ public static final int CALL_TYPE_INCOMING = 1;
+ /**
+ * Call type for ongoing calls.
+ *
+ * See {@link #EXTRA_CALL_TYPE}.
+ */
+ public static final int CALL_TYPE_ONGOING = 2;
+ /**
+ * Call type for calls that are being screened.
+ *
+ * See {@link #EXTRA_CALL_TYPE}.
+ */
+ public static final int CALL_TYPE_SCREENING = 3;
+
+ /**
+ * This is a key used privately on the action.extras to give spacing priority
+ * to the required call actions
+ */
+ private static final String KEY_ACTION_PRIORITY = "key_action_priority";
+
+ private int mCallType;
+ private Person mPerson;
+ private PendingIntent mAnswerIntent;
+ private PendingIntent mDeclineIntent;
+ private PendingIntent mHangUpIntent;
+ private boolean mIsVideo;
+ private Integer mAnswerButtonColor;
+ private Integer mDeclineButtonColor;
+ private IconCompat mVerificationIcon;
+ private CharSequence mVerificationText;
+
+ public CallStyle() {
+ }
+
+ /**
+ * Creates a CallStyle linked to a notification builder.
+ *
+ * @param builder the notification builder to link
+ */
+ public CallStyle(@Nullable Builder builder) {
+ setBuilder(builder);
+ }
+
+ /**
+ * Creates a CallStyle for an incoming call.
+ * This notification will have a decline and an answer action, will allow a single
+ * custom {@link Builder#addAction(Action) action}, and will have a default
+ * {@link Builder#setContentText(CharSequence) content text} for an incoming call.
+ *
+ * @param person the person displayed as the caller
+ * the person also needs to have a non-empty name associated with it
+ * @param declineIntent the intent to be sent when the user taps the decline action
+ * @param answerIntent the intent to be sent when the user taps the answer action
+ */
+ @NonNull
+ public static CallStyle forIncomingCall(@NonNull Person person,
+ @NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent) {
+ return new CallStyle(CALL_TYPE_INCOMING, person,
+ null /* hangUpIntent */,
+ requireNonNull(declineIntent, "declineIntent is required"),
+ requireNonNull(answerIntent, "answerIntent is required")
+ );
+ }
+
+ /**
+ * Creates a CallStyle for an ongoing call.
+ * This notification will have a hang up action, will allow up to two
+ * custom {@link Builder#addAction(Action) actions}, and will have a default
+ * {@link Builder#setContentText(CharSequence) content text} for an ongoing call.
+ *
+ * @param person the person displayed as being on the other end of the call
+ * the person also needs to have a non-empty name associated with it
+ * @param hangUpIntent the intent to be sent when the user taps the hang up action
+ */
+ @NonNull
+ public static CallStyle forOngoingCall(@NonNull Person person,
+ @NonNull PendingIntent hangUpIntent) {
+ return new CallStyle(CALL_TYPE_ONGOING, person,
+ requireNonNull(hangUpIntent, "hangUpIntent is required"),
+ null /* declineIntent */,
+ null /* answerIntent */
+ );
+ }
+
+ /**
+ * Creates a CallStyle for a call that is being screened.
+ * This notification will have a hang up and an answer action, will allow a single
+ * custom {@link Builder#addAction(Action) action}, and will have a default
+ * {@link Builder#setContentText(CharSequence) content text} for a call that is being
+ * screened.
+ *
+ * @param person the person displayed as the caller
+ * the person also needs to have a non-empty name associated with it
+ * @param hangUpIntent the intent to be sent when the user taps the hang up action
+ * @param answerIntent the intent to be sent when the user taps the answer action
+ */
+ @NonNull
+ public static CallStyle forScreeningCall(@NonNull Person person,
+ @NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent) {
+ return new CallStyle(CALL_TYPE_SCREENING, person,
+ requireNonNull(hangUpIntent, "hangUpIntent is required"),
+ null /* declineIntent */,
+ requireNonNull(answerIntent, "answerIntent is required")
+ );
+ }
+
+ /**
+ * @param callType the type of the call (for example, CALL_TYPE_INCOMING)
+ * @param person the person displayed for the incoming call
+ * the user also needs to have a non-empty name associated with it
+ * @param hangUpIntent the intent to be sent when the user taps the hang up action
+ * @param declineIntent the intent to be sent when the user taps the decline action
+ * @param answerIntent the intent to be sent when the user taps the answer action
+ */
+ private CallStyle(@CallType int callType, @NonNull Person person,
+ @Nullable PendingIntent hangUpIntent, @Nullable PendingIntent declineIntent,
+ @Nullable PendingIntent answerIntent) {
+ if (person == null || TextUtils.isEmpty(person.getName())) {
+ throw new IllegalArgumentException("person must have a non-empty a name");
+ }
+ mCallType = callType;
+ mPerson = person;
+ mAnswerIntent = answerIntent;
+ mDeclineIntent = declineIntent;
+ mHangUpIntent = hangUpIntent;
+ }
+
+ /**
+ * Sets whether the call is a video call, which may affect the icons or text used on the
+ * required action buttons.
+ */
+ @NonNull
+ public CallStyle setIsVideo(boolean isVideo) {
+ mIsVideo = isVideo;
+ return this;
+ }
+
+ /**
+ * Sets an optional icon to be displayed with {@link #setVerificationText(CharSequence)
+ * text} as a verification status of the caller.
+ */
+ @RequiresApi(23)
+ @NonNull
+ public CallStyle setVerificationIcon(@Nullable Icon verificationIcon) {
+ mVerificationIcon = verificationIcon == null ? null :
+ IconCompat.createFromIcon(verificationIcon);
+ return this;
+ }
+
+ /**
+ * Sets an optional icon to be displayed with {@link #setVerificationText(CharSequence)
+ * text} as a verification status of the caller.
+ */
+ @NonNull
+ public CallStyle setVerificationIcon(@Nullable Bitmap verificationIcon) {
+ mVerificationIcon = IconCompat.createWithBitmap(verificationIcon);
+ return this;
+ }
+
+ /**
+ * Sets optional text to be displayed with an {@link #setVerificationIcon(Icon) icon}
+ * as a verification status of the caller.
+ */
+ @NonNull
+ public CallStyle setVerificationText(@Nullable CharSequence verificationText) {
+ mVerificationText = verificationText;
+ return this;
+ }
+
+ /**
+ * Sets an optional color to be used as a hint for the Answer action button's color.
+ * The system may change this color to ensure sufficient contrast with the background.
+ * The system may choose to disregard this hint if the notification is not colorized.
+ */
+ @NonNull
+ public CallStyle setAnswerButtonColorHint(@ColorInt int color) {
+ mAnswerButtonColor = color;
+ return this;
+ }
+
+ /**
+ * Sets an optional color to be used as a hint for the Decline or Hang Up action button's
+ * color.
+ * The system may change this color to ensure sufficient contrast with the background.
+ * The system may choose to disregard this hint if the notification is not colorized.
+ */
+ @NonNull
+ public CallStyle setDeclineButtonColorHint(@ColorInt int color) {
+ mDeclineButtonColor = color;
+ return this;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public boolean displayCustomViewInline() {
+ // This is a lie; True is returned to make sure that the custom view is not used
+ // instead of the template, but it will not actually be included.
+ return true;
+ }
+
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ protected void restoreFromCompatExtras(@NonNull Bundle extras) {
+ super.restoreFromCompatExtras(extras);
+
+ mCallType = extras.getInt(EXTRA_CALL_TYPE);
+ mIsVideo = extras.getBoolean(EXTRA_CALL_IS_VIDEO);
+ if (Build.VERSION.SDK_INT >= 28
+ && extras.containsKey(EXTRA_CALL_PERSON)) {
+ mPerson = Person.fromAndroidPerson(
+ (android.app.Person)
+ extras.getParcelable(EXTRA_CALL_PERSON));
+ } else if (extras.containsKey(EXTRA_CALL_PERSON_COMPAT)) {
+ mPerson = Person.fromBundle(extras.getBundle(EXTRA_CALL_PERSON_COMPAT));
+ }
+ if (Build.VERSION.SDK_INT >= 23 && extras.containsKey(EXTRA_VERIFICATION_ICON)) {
+ mVerificationIcon = IconCompat.createFromIcon((Icon) extras.getParcelable(
+ EXTRA_VERIFICATION_ICON));
+ } else if (extras.containsKey(EXTRA_VERIFICATION_ICON_COMPAT)) {
+ mVerificationIcon =
+ IconCompat.createFromBundle(
+ extras.getBundle(EXTRA_VERIFICATION_ICON_COMPAT));
+ }
+ mVerificationText = extras.getCharSequence(EXTRA_VERIFICATION_TEXT);
+ mAnswerIntent = (PendingIntent) extras.getParcelable(EXTRA_ANSWER_INTENT);
+ mDeclineIntent = (PendingIntent) extras.getParcelable(EXTRA_DECLINE_INTENT);
+ mHangUpIntent = (PendingIntent) extras.getParcelable(EXTRA_HANG_UP_INTENT);
+ mAnswerButtonColor = extras.containsKey(EXTRA_ANSWER_COLOR)
+ ? extras.getInt(EXTRA_ANSWER_COLOR) : null;
+ mDeclineButtonColor = extras.containsKey(EXTRA_DECLINE_COLOR)
+ ? extras.getInt(EXTRA_DECLINE_COLOR) : null;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void addCompatExtras(@NonNull Bundle extras) {
+ super.addCompatExtras(extras);
+ // Reminder: this method only needs to add fields which are not added by the platform
+ // builder (and only needs to work at all for API 19+).
+
+ extras.putInt(EXTRA_CALL_TYPE, mCallType);
+ extras.putBoolean(EXTRA_CALL_IS_VIDEO, mIsVideo);
+ if (mPerson != null) {
+ if (Build.VERSION.SDK_INT >= 28) {
+ extras.putParcelable(EXTRA_CALL_PERSON,
+ Api28Impl.castToParcelable(mPerson.toAndroidPerson()));
+ } else {
+ extras.putParcelable(EXTRA_CALL_PERSON_COMPAT, mPerson.toBundle());
+ }
+ }
+ if (mVerificationIcon != null) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ extras.putParcelable(EXTRA_VERIFICATION_ICON, Api23Impl.castToParcelable(
+ mVerificationIcon.toIcon(mBuilder.mContext)));
+ } else {
+ extras.putParcelable(EXTRA_VERIFICATION_ICON_COMPAT,
+ mVerificationIcon.toBundle());
+ }
+ }
+ extras.putCharSequence(EXTRA_VERIFICATION_TEXT, mVerificationText);
+ extras.putParcelable(EXTRA_ANSWER_INTENT, mAnswerIntent);
+ extras.putParcelable(EXTRA_DECLINE_INTENT, mDeclineIntent);
+ extras.putParcelable(EXTRA_HANG_UP_INTENT, mHangUpIntent);
+ if (mAnswerButtonColor != null) {
+ extras.putInt(EXTRA_ANSWER_COLOR, mAnswerButtonColor);
+ }
+ if (mDeclineButtonColor != null) {
+ extras.putInt(EXTRA_DECLINE_COLOR, mDeclineButtonColor);
+ }
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @NonNull
+ protected String getClassName() {
+ return TEMPLATE_CLASS_NAME;
+ }
+
+ /**
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builderAccessor) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ Notification.CallStyle style = null;
+ switch (mCallType) {
+ case CALL_TYPE_INCOMING:
+ style = Api31Impl.forIncomingCall(mPerson.toAndroidPerson(),
+ mDeclineIntent, mAnswerIntent);
+ break;
+ case CALL_TYPE_ONGOING:
+ style = Api31Impl.forOngoingCall(mPerson.toAndroidPerson(),
+ mHangUpIntent);
+ break;
+ case CALL_TYPE_SCREENING:
+ style = Api31Impl.forScreeningCall(mPerson.toAndroidPerson(),
+ mHangUpIntent, mAnswerIntent);
+ break;
+ default:
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG,
+ "Unrecognized call type in CallStyle: " + String.valueOf(
+ mCallType));
+ }
+ }
+ if (style != null) {
+ style.setBuilder(builderAccessor.getBuilder());
+ if (mAnswerButtonColor != null) {
+ Api31Impl.setAnswerButtonColorHint(style, mAnswerButtonColor);
+ }
+ if (mDeclineButtonColor != null) {
+ Api31Impl.setDeclineButtonColorHint(style, mDeclineButtonColor);
+ }
+ Api31Impl.setVerificationText(style, mVerificationText);
+ if (mVerificationIcon != null) {
+ Api31Impl.setVerificationIcon(style,
+ mVerificationIcon.toIcon(mBuilder.mContext));
+ }
+ Api31Impl.setIsVideo(style, mIsVideo);
+ }
+ } else {
+ // For versions before CallStyle existed, we fallback to an unstyled
+ // notification, and modify the builder directly to set the relevant fields.
+ // Fields not settable in the builder are added separately as part of the
+ // RemoteView.
+ Notification.Builder builder = builderAccessor.getBuilder();
+
+ // Sets the notification title to the caller name.
+ CharSequence title = mPerson != null ? mPerson.getName() : null;
+ builder.setContentTitle(title);
+
+ // Sets the text of the notification either to EXTRA_TEXT, or (if not set),
+ // uses the default call notification text.
+ CharSequence text =
+ mBuilder.mExtras != null && mBuilder.mExtras.containsKey(EXTRA_TEXT)
+ ? mBuilder.mExtras.getCharSequence(EXTRA_TEXT) : null;
+ if (text == null) {
+ text = getDefaultText();
+ }
+ builder.setContentText(text);
+
+ // Adds person information to the notification.
+ if (mPerson != null) {
+ // Adds the caller icon, if available.
+ if (Build.VERSION.SDK_INT >= 23 && mPerson.getIcon() != null) {
+ Api23Impl.setLargeIcon(builder,
+ mPerson.getIcon().toIcon(mBuilder.mContext));
+ }
+
+ // Adds the caller person as being relevant to this notification.
+ if (Build.VERSION.SDK_INT >= 28) {
+ Api28Impl.addPerson(builder, mPerson.toAndroidPerson());
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ Api21Impl.addPerson(builder, mPerson.getUri());
+ }
+ }
+
+ // Sets the category of the notification to CATEGORY_CALL; if the notification
+ // has this set and is also from the default phone app, it will be ranked in the
+ // shade similarly to how CallStyle notifications are ranked in API 31+.
+ if (Build.VERSION.SDK_INT >= 21) {
+ Api21Impl.setCategory(builder, NotificationCompat.CATEGORY_CALL);
+ }
+ }
+ }
+
+ /**
+ * Provides the default text for a CallStyle notification. Corresponds to Notification
+ * .CallStyle
+ */
+ @Nullable
+ private String getDefaultText() {
+ switch (mCallType) {
+ case CALL_TYPE_INCOMING:
+ return mBuilder.mContext.getResources().getString(
+ R.string.call_notification_incoming_text);
+ case CALL_TYPE_ONGOING:
+ return mBuilder.mContext.getResources().getString(
+ R.string.call_notification_ongoing_text);
+ case CALL_TYPE_SCREENING:
+ return mBuilder.mContext.getResources().getString(
+ R.string.call_notification_screening_text);
+ }
+ return null;
+ }
+
+ @NonNull
+ @RequiresApi(20)
+ private Action makeNegativeAction() {
+ int icon = R.drawable.ic_call_decline_low;
+ if (Build.VERSION.SDK_INT >= 21) {
+ icon = R.drawable.ic_call_decline;
+ }
+ if (mDeclineIntent == null) {
+ return makeAction(icon, R.string.call_notification_hang_up_action,
+ mDeclineButtonColor,
+ R.color.call_notification_decline_color,
+ mHangUpIntent);
+ } else {
+ return makeAction(icon, R.string.call_notification_decline_action,
+ mDeclineButtonColor,
+ R.color.call_notification_decline_color,
+ mDeclineIntent);
+ }
+ }
+
+ @Nullable
+ @RequiresApi(20)
+ private Action makeAnswerAction() {
+ int videoIcon = R.drawable.ic_call_answer_video_low;
+ int icon = R.drawable.ic_call_answer_low;
+ if (Build.VERSION.SDK_INT >= 21) {
+ videoIcon = R.drawable.ic_call_answer_video;
+ icon = R.drawable.ic_call_answer;
+ }
+
+ return mAnswerIntent == null ? null : makeAction(
+ mIsVideo ? videoIcon : icon,
+ mIsVideo ? R.string.call_notification_answer_video_action
+ : R.string.call_notification_answer_action,
+ mAnswerButtonColor, R.color.call_notification_answer_color,
+ mAnswerIntent);
+ }
+
+ @NonNull
+ @RequiresApi(20)
+ private Action makeAction(int icon, int title, Integer colorInt, int defaultColorRes,
+ PendingIntent intent) {
+ if (colorInt == null) {
+ colorInt = ContextCompat.getColor(mBuilder.mContext, defaultColorRes);
+ }
+ SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
+ stringBuilder.append(mBuilder.mContext.getResources().getString(title));
+ stringBuilder.setSpan(new ForegroundColorSpan(colorInt), 0, stringBuilder.length(),
+ SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
+
+ Action action = new Action.Builder(
+ IconCompat.createWithResource(mBuilder.mContext, icon), stringBuilder,
+ intent).build();
+ action.getExtras().putBoolean(KEY_ACTION_PRIORITY, true);
+ return action;
+ }
+
+ private boolean isActionAddedByCallStyle(Action action) {
+ // This is an internal extra added by the style to these actions. If an app were to add
+ // this extra to the action themselves, the action would be dropped. :shrug:
+ return action != null && action.getExtras().getBoolean(KEY_ACTION_PRIORITY);
+ }
+
+ /**
+ * Gets the actions list for the call with the answer/decline/hangUp actions inserted in
+ * the correct place. This returns the correct result even if the system actions have
+ * already been added, and even if more actions were added since then.
+ *
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @NonNull
+ @RequiresApi(20)
+ public ArrayList
+ * If the platform does not provide large-format notifications, this method has no effect. The
+ * user will always see the normal notification view.
+ *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so:
+ *
+ * Notification notification = new Notification.Builder()
+ * .setContentTitle("5 New mails from " + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.InboxStyle()
+ * .addLine(str1)
+ * .addLine(str2)
+ * .setContentTitle("")
+ * .setSummaryText("+3 more"))
+ * .build();
+ *
+ *
+ * @see Notification#bigContentView
+ */
+ public static class InboxStyle extends Style {
+
+ private static final String TEMPLATE_CLASS_NAME =
+ "androidx.core.app.NotificationCompat$InboxStyle";
+
+ private ArrayList
+ * Notification noti = new NotificationCompat.Builder()
+ * .setSmallIcon(R.drawable.ic_stat_player)
+ * .setLargeIcon(albumArtBitmap))
+ * .setCustomContentView(contentView)
+ * .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
+ * .build();
+ *
+ *
+ *
+ * NotificationCompat.Action action = new NotificationCompat.Action.Builder(
+ * R.drawable.archive_all, "Archive all", actionIntent)
+ * .extend(new NotificationCompat.Action.WearableExtender()
+ * .setAvailableOffline(false))
+ * .build();
+ */
+ public static final class WearableExtender implements Extender {
+ /** Notification action extra which contains wearable extensions */
+ private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS";
+
+ // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options.
+ private static final String KEY_FLAGS = "flags";
+ private static final String KEY_IN_PROGRESS_LABEL = "inProgressLabel";
+ private static final String KEY_CONFIRM_LABEL = "confirmLabel";
+ private static final String KEY_CANCEL_LABEL = "cancelLabel";
+
+ // Flags bitwise-ored to mFlags
+ private static final int FLAG_AVAILABLE_OFFLINE = 0x1;
+ private static final int FLAG_HINT_LAUNCHES_ACTIVITY = 1 << 1;
+ private static final int FLAG_HINT_DISPLAY_INLINE = 1 << 2;
+
+ // Default value for flags integer
+ private static final int DEFAULT_FLAGS = FLAG_AVAILABLE_OFFLINE;
+
+ private int mFlags = DEFAULT_FLAGS;
+
+ private CharSequence mInProgressLabel;
+ private CharSequence mConfirmLabel;
+ private CharSequence mCancelLabel;
+
+ /**
+ * Create a {@link NotificationCompat.Action.WearableExtender} with default
+ * options.
+ */
+ public WearableExtender() {
+ }
+
+ /**
+ * Create a {@link NotificationCompat.Action.WearableExtender} by reading
+ * wearable options present in an existing notification action.
+ * @param action the notification action to inspect.
+ */
+ public WearableExtender(@NonNull Action action) {
+ Bundle wearableBundle = action.getExtras().getBundle(EXTRA_WEARABLE_EXTENSIONS);
+ if (wearableBundle != null) {
+ mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS);
+ mInProgressLabel = wearableBundle.getCharSequence(KEY_IN_PROGRESS_LABEL);
+ mConfirmLabel = wearableBundle.getCharSequence(KEY_CONFIRM_LABEL);
+ mCancelLabel = wearableBundle.getCharSequence(KEY_CANCEL_LABEL);
+ }
+ }
+
+ /**
+ * Apply wearable extensions to a notification action that is being built. This is
+ * typically called by the {@link NotificationCompat.Action.Builder#extend}
+ * method of {@link NotificationCompat.Action.Builder}.
+ */
+ @Override
+ public @NonNull Action.Builder extend(@NonNull Action.Builder builder) {
+ Bundle wearableBundle = new Bundle();
+
+ if (mFlags != DEFAULT_FLAGS) {
+ wearableBundle.putInt(KEY_FLAGS, mFlags);
+ }
+ if (mInProgressLabel != null) {
+ wearableBundle.putCharSequence(KEY_IN_PROGRESS_LABEL, mInProgressLabel);
+ }
+ if (mConfirmLabel != null) {
+ wearableBundle.putCharSequence(KEY_CONFIRM_LABEL, mConfirmLabel);
+ }
+ if (mCancelLabel != null) {
+ wearableBundle.putCharSequence(KEY_CANCEL_LABEL, mCancelLabel);
+ }
+
+ builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle);
+ return builder;
+ }
+
+ @Override
+ public @NonNull WearableExtender clone() {
+ WearableExtender that = new WearableExtender();
+ that.mFlags = this.mFlags;
+ that.mInProgressLabel = this.mInProgressLabel;
+ that.mConfirmLabel = this.mConfirmLabel;
+ that.mCancelLabel = this.mCancelLabel;
+ return that;
+ }
+
+ /**
+ * Set whether this action is available when the wearable device is not connected to
+ * a companion device. The user can still trigger this action when the wearable device
+ * is offline, but a visual hint will indicate that the action may not be available.
+ * Defaults to true.
+ */
+ public @NonNull WearableExtender setAvailableOffline(boolean availableOffline) {
+ setFlag(FLAG_AVAILABLE_OFFLINE, availableOffline);
+ return this;
+ }
+
+ /**
+ * Get whether this action is available when the wearable device is not connected to
+ * a companion device. The user can still trigger this action when the wearable device
+ * is offline, but a visual hint will indicate that the action may not be available.
+ * Defaults to true.
+ */
+ public boolean isAvailableOffline() {
+ return (mFlags & FLAG_AVAILABLE_OFFLINE) != 0;
+ }
+
+ private void setFlag(int mask, boolean value) {
+ if (value) {
+ mFlags |= mask;
+ } else {
+ mFlags &= ~mask;
+ }
+ }
+
+ /**
+ * Set a label to display while the wearable is preparing to automatically execute the
+ * action. This is usually a 'ing' verb ending in ellipsis like "Sending..."
+ *
+ * @param label the label to display while the action is being prepared to execute
+ * @return this object for method chaining
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setInProgressLabel(@Nullable CharSequence label) {
+ mInProgressLabel = label;
+ return this;
+ }
+
+ /**
+ * Get the label to display while the wearable is preparing to automatically execute
+ * the action. This is usually a 'ing' verb ending in ellipsis like "Sending..."
+ *
+ * @return the label to display while the action is being prepared to execute
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @Nullable CharSequence getInProgressLabel() {
+ return mInProgressLabel;
+ }
+
+ /**
+ * Set a label to display to confirm that the action should be executed.
+ * This is usually an imperative verb like "Send".
+ *
+ * @param label the label to confirm the action should be executed
+ * @return this object for method chaining
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setConfirmLabel(@Nullable CharSequence label) {
+ mConfirmLabel = label;
+ return this;
+ }
+
+ /**
+ * Get the label to display to confirm that the action should be executed.
+ * This is usually an imperative verb like "Send".
+ *
+ * @return the label to confirm the action should be executed
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ @Nullable
+ public CharSequence getConfirmLabel() {
+ return mConfirmLabel;
+ }
+
+ /**
+ * Set a label to display to cancel the action.
+ * This is usually an imperative verb, like "Cancel".
+ *
+ * @param label the label to display to cancel the action
+ * @return this object for method chaining
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setCancelLabel(@Nullable CharSequence label) {
+ mCancelLabel = label;
+ return this;
+ }
+
+ /**
+ * Get the label to display to cancel the action.
+ * This is usually an imperative verb like "Cancel".
+ *
+ * @return the label to display to cancel the action
+ *
+ * @deprecated This method has no effect starting with Wear 2.0.
+ */
+ @Deprecated
+ public @Nullable CharSequence getCancelLabel() {
+ return mCancelLabel;
+ }
+
+ /**
+ * Set a hint that this Action will launch an {@link Activity} directly, telling the
+ * platform that it can generate the appropriate transitions.
+ * @param hintLaunchesActivity {@code true} if the content intent will launch
+ * an activity and transitions should be generated, false otherwise.
+ * @return this object for method chaining
+ */
+ public @NonNull WearableExtender setHintLaunchesActivity(
+ boolean hintLaunchesActivity) {
+ setFlag(FLAG_HINT_LAUNCHES_ACTIVITY, hintLaunchesActivity);
+ return this;
+ }
+
+ /**
+ * Get a hint that this Action will launch an {@link Activity} directly, telling the
+ * platform that it can generate the appropriate transitions
+ * @return {@code true} if the content intent will launch an activity and transitions
+ * should be generated, false otherwise. The default value is {@code false} if this was
+ * never set.
+ */
+ public boolean getHintLaunchesActivity() {
+ return (mFlags & FLAG_HINT_LAUNCHES_ACTIVITY) != 0;
+ }
+
+ /**
+ * Set a hint that this Action should be displayed inline - i.e. it will have a visual
+ * representation directly on the notification surface in addition to the expanded
+ * Notification
+ *
+ * @param hintDisplayInline {@code true} if action should be displayed inline, false
+ * otherwise
+ * @return this object for method chaining
+ */
+ public @NonNull WearableExtender setHintDisplayActionInline(
+ boolean hintDisplayInline) {
+ setFlag(FLAG_HINT_DISPLAY_INLINE, hintDisplayInline);
+ return this;
+ }
+
+ /**
+ * Get a hint that this Action should be displayed inline - i.e. it should have a
+ * visual representation directly on the notification surface in addition to the
+ * expanded Notification
+ *
+ * @return {@code true} if the Action should be displayed inline, {@code false}
+ * otherwise. The default value is {@code false} if this was never set.
+ */
+ public boolean getHintDisplayActionInline() {
+ return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0;
+ }
+ }
+
+ /**
+ * Provides meaning to an {@link Action} that hints at what the associated
+ * {@link PendingIntent} will do. For example, an {@link Action} with a
+ * {@link PendingIntent} that replies to a text message notification may have the
+ * {@link #SEMANTIC_ACTION_REPLY} {@link SemanticAction} set within it.
+ */
+ @IntDef({
+ SEMANTIC_ACTION_NONE,
+ SEMANTIC_ACTION_REPLY,
+ SEMANTIC_ACTION_MARK_AS_READ,
+ SEMANTIC_ACTION_MARK_AS_UNREAD,
+ SEMANTIC_ACTION_DELETE,
+ SEMANTIC_ACTION_ARCHIVE,
+ SEMANTIC_ACTION_MUTE,
+ SEMANTIC_ACTION_UNMUTE,
+ SEMANTIC_ACTION_THUMBS_UP,
+ SEMANTIC_ACTION_THUMBS_DOWN,
+ SEMANTIC_ACTION_CALL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SemanticAction {}
+ }
+
+
+ /**
+ * Extender interface for use with {@link Builder#extend}. Extenders may be used to add
+ * metadata or change options on a notification builder.
+ */
+ public interface Extender {
+ /**
+ * Apply this extender to a notification builder.
+ * @param builder the builder to be modified.
+ * @return the build object for chaining.
+ */
+ @NonNull Builder extend(@NonNull Builder builder);
+ }
+
+ /**
+ * Helper class to add wearable extensions to notifications.
+ *
+ *
+ *
+ *
+ * Notification notification = new NotificationCompat.Builder(mContext)
+ * .setContentTitle("New mail from " + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .extend(new NotificationCompat.WearableExtender()
+ * .setContentIcon(R.drawable.new_mail))
+ * .build();
+ * NotificationManagerCompat.from(mContext).notify(0, notification);
+ *
+ *
+ * NotificationCompat.WearableExtender wearableExtender =
+ * new NotificationCompat.WearableExtender(notification);
+ * List<Notification> pages = wearableExtender.getPages();
+ */
+ public static final class WearableExtender implements Extender {
+ /**
+ * Sentinel value for an action index that is unset.
+ */
+ public static final int UNSET_ACTION_INDEX = -1;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification with
+ * default sizing.
+ *
+ * Intent displayIntent = new Intent(context, MyDisplayActivity.class);
+ * PendingIntent displayPendingIntent = PendingIntent.getActivity(context,
+ * 0, displayIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ * Notification notification = new NotificationCompat.Builder(context)
+ * .extend(new NotificationCompat.WearableExtender()
+ * .setDisplayIntent(displayPendingIntent)
+ * .setCustomSizePreset(NotificationCompat.WearableExtender.SIZE_MEDIUM))
+ * .build();
+ *
+ *
+ * <activity android:name="com.example.MyDisplayActivity"
+ * android:exported="true"
+ * android:allowEmbedded="true"
+ * android:taskAffinity=""
+ * android:theme="@android:style/Theme.DeviceDefault.Light" />
+ *
+ * @param intent the {@link PendingIntent} for an activity
+ * @return this object for method chaining
+ * @see NotificationCompat.WearableExtender#getDisplayIntent
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public @NonNull WearableExtender setDisplayIntent(@Nullable PendingIntent intent) {
+ mDisplayIntent = intent;
+ return this;
+ }
+
+ /**
+ * Get the intent to launch inside of an activity view when displaying this
+ * notification. This {@code PendingIntent} should be for an activity.
+ *
+ * @deprecated Display intents are no longer supported.
+ */
+ @Deprecated
+ public @Nullable PendingIntent getDisplayIntent() {
+ return mDisplayIntent;
+ }
+
+ /**
+ * Add an additional page of content to display with this notification. The current
+ * notification forms the first page, and pages added using this function form
+ * subsequent pages. This field can be used to separate a notification into multiple
+ * sections.
+ *
+ * @param page the notification to add as another page
+ * @return this object for method chaining
+ * @see NotificationCompat.WearableExtender#getPages
+ * @deprecated Multiple content pages are no longer supported.
+ */
+ @Deprecated
+ public @NonNull WearableExtender addPage(@NonNull Notification page) {
+ mPages.add(page);
+ return this;
+ }
+
+ /**
+ * Add additional pages of content to display with this notification. The current
+ * notification forms the first page, and pages added using this function form
+ * subsequent pages. This field can be used to separate a notification into multiple
+ * sections.
+ *
+ * @param pages a list of notifications
+ * @return this object for method chaining
+ * @see NotificationCompat.WearableExtender#getPages
+ * @deprecated Multiple content pages are no longer supported.
+ */
+ @Deprecated
+ public @NonNull WearableExtender addPages(@NonNull List
+ *
+ *
+ *
+ * Notification notification = new NotificationCompat.Builder(context)
+ * ...
+ * .extend(new CarExtender()
+ * .set*(...))
+ * .build();
+ *
+ *
+ *
+ *
+ *
+ *
+ * Notification notification = new NotificationCompat.Builder(context)
+ * ...
+ * .extend(new TvExtender()
+ * .setChannelId("channel id"))
+ * .build();
+ * NotificationManagerCompat.from(mContext).notify(0, notification);
+ *
+ *
+ * true the notification is
+ * hidden, when false the notification shows as normal.
+ *
+ * true to hide the notification
+ * from the user once the bubble has been expanded.
+ *
+ *
+ *
+ *
+ *
+ *